Cassandra

How to build a Spring + Cassandra + Rest application

1. Overview

This tutorial will provide an introduction to Spring Data JPA having a Spring project using Java 17, Lombok, Apache Cassandra, using a fully Reactive approach with Cassandra and Spring WebFlux, it will cover configuring the persistence layer. For a step-by-step introduction to setting up the Spring context using Java-based configuration and you can checkout the projection configuration and sources from our repository

2. Cassandra

Cassandra is a free and open-source, distributed, wide-column store, NoSQL database management system designed to handle large amounts of data across many commodity servers, providing high availability with no single point of failure. Cassandra offers support for clusters spanning multiple datacenters, with asynchronous masterless replication allowing low latency operations for all clients. Cassandra was designed to implement a combination of Amazon's Dynamo distributed storage and replication techniques combined with Google's Bigtable data and storage engine model.

3. Table

In the NoSQL world Cassandra unlike Mongo uses BigTable, that means is a denormalize store making all information centrallize into a single table what makes Cassandra a perfect data storage for a microservice since it will not support table relations that will force the domain model to be simple and only focus on a single responsibility, therefore our Pojo will define this Table and it's fields

                

import lombok.*;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;


@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table
public class User {
    @PrimaryKey
    private String id;
    private String fullName;
    private String email;
}                                      
                
            

User.java

                

import org.springframework.data.cassandra.core.mapping.PrimaryKey
import org.springframework.data.cassandra.core.mapping.Table


@Table
class User (
    @PrimaryKey
    var id: String? = null,
    var fullName: String,
    var email: String
)

                    
                
            

User.kt

                
import org.springframework.data.cassandra.core.mapping.{PrimaryKey, Table}
import scala.beans.BeanProperty

@Table
class User (@BeanProperty
            var fullName: String,
            @BeanProperty
            var email: String){
  @PrimaryKey
  var id: String = _
  def this() = this(null,null)
}

                    
                
            

User.scala

4. Repository

Spring Data provides a repository layer that allows to avoid the boilerplate code of the Cassandra queries (CQL)

                
import com.the.sample.app.model.User;
import org.springframework.data.cassandra.repository.ReactiveCassandraRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;

@Repository
public interface UserRepository extends ReactiveCassandraRepository {
    Mono findByEmail(String email);
}                                   
                
            

UserRepository.java

                

import com.the.sample.app.model.User
import org.springframework.data.cassandra.repository.ReactiveCassandraRepository
import org.springframework.stereotype.Repository
import reactor.core.publisher.Mono

@Repository
interface UserRepository : ReactiveCassandraRepository {
    fun findByEmail(email: String?): Mono
}

                    
                
            

UserRepository.kt

                

import com.the.sample.app.model.User
import org.springframework.data.cassandra.repository.ReactiveCassandraRepository
import org.springframework.stereotype.Repository
import reactor.core.publisher.Mono

@Repository
trait UserRepository extends ReactiveCassandraRepository[User,String]{
  def findByEmail(email: String): Mono[User]
}

                    
                
            

UserRepository.scala

5. Service

The service layer adds the business layer that we will need between our integration and the repository layers

                
import com.the.sample.app.model.User;
import com.the.sample.app.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.UUID;

@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService{
    private final UserRepository userRepository;

    @Override
    public Flux findAll() {
        return userRepository.findAll();
    }

    @Override
    public Mono findById(String id) {
        return userRepository.findById(id);
    }

    @Override
    public Mono findByEmail(String email) {
        return userRepository.findByEmail(email);
    }

    @Override
    public void save(User user) {
        user.setId(UUID.randomUUID().toString());
        userRepository.save(user);
    }

    @Override
    public void deleteById(String id) {
        userRepository.deleteById(id);
    }
}
                               
                
            

UserServiceImpl.java

                
import com.the.sample.app.model.User
import com.the.sample.app.repository.UserRepository
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.util.*

interface UserService {
    fun findAll(): Flux
    fun findById(id: String): Mono
    fun findByEmail(email: String): Mono
    fun save(user: User)
    fun deleteById(id: String)
}

@Service
class UserServiceImpl(val userRepository: UserRepository) : UserService{
    override fun findAll(): Flux {
        return userRepository.findAll()
    }
    override fun findById(id: String): Mono {
        return userRepository.findById(id)
    }

    override fun findByEmail(email: String): Mono {
        return userRepository.findByEmail(email)
    }

    override fun save(user: User) {
        user.id = UUID.randomUUID().toString()
        userRepository.save(user)
    }

    override fun deleteById(id: String) {
        userRepository.deleteById(id)
    }
}

                    
                
            

UserService.kt

                
import com.the.sample.app.model.User
import com.the.sample.app.repository.UserRepository
import org.springframework.stereotype.Service
import reactor.core.publisher.{Flux, Mono}

trait UserService {
  def findAll(): Flux[User]

  def findById(id: String): Mono[User]

  def findByEmail(email: String): Mono[User]

  def save(user: User): Unit

  def deleteById(id: String): Unit
}

@Service
class UserServiceImpl(userRepository: UserRepository) extends UserService {
  override def findAll(): Flux[User] =
    userRepository.findAll()

  override def findById(id: String): Mono[User] = userRepository.findById(id)

  override def findByEmail(email: String): Mono[User] = userRepository.findByEmail(email)

  override def save(user: User): Unit = userRepository.save(user)

  override def deleteById(id: String): Unit = userRepository.deleteById(id)
}

                    
                
            

UserService.scala

6. Rest Controller

This is our integration layer where our microservice can be called by Rest JSON using Spring WebFlux using the new Spring Reactive Stack

                
import com.the.sample.app.model.User;
import com.the.sample.app.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserRestController {
    private final UserService userService;

    @GetMapping("/")
    public Flux findAll() {
        return userService.findAll();
    }

    @GetMapping("/{id}")
    public Mono findUserById(@PathVariable("id") String id) {
        return userService.findById(id);
    }


    @PostMapping("/")
    public Mono saverUser(@RequestBody User user) {
        userService.save(user);
        return Mono.just(user);
    }

    @PutMapping("/{id}")
    public Mono updateUser(@PathVariable("id") String id,@RequestBody User user) {
        user.setId(id);
        userService.save(user);
        return Mono.just(user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable("id") String id) {
        userService.deleteById(id);
    }
}                               
                
            

UserRestController.java

                
import com.the.sample.app.model.User
import com.the.sample.app.service.UserService
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

@RestController
@RequestMapping("/users")
class UserRestController(val userService: UserService) {

    @GetMapping("/{id}")
    fun getUserById(@PathVariable("id") id: String): Mono {
        return userService.findById(id)
    }

    @GetMapping("/")
    fun getAllUsers(): Flux {
        return userService.findAll()
    }

    @PostMapping("/")
    fun saverUser(@RequestBody user: User): Mono {
        userService.save(user)
        return Mono.just(user)
    }

    @PutMapping("/{id}")
    fun updateUser(@PathVariable("id") id: String, @RequestBody user: User): Mono? {
        user.id = id
        userService.save(user)
        return Mono.just(user)
    }

    @DeleteMapping("/{id}")
    fun deleteUser(@PathVariable("id") id: String): Unit {
        userService.deleteById(id)
    }
}

                    
                
            

UserRestController.kt

                
import com.the.sample.app.model.User
import com.the.sample.app.service.UserService
import org.springframework.web.bind.annotation.{
  DeleteMapping,
  GetMapping,
  PathVariable,
  PostMapping,
  PutMapping,
  RequestBody,
  RequestMapping,
  RestController
}
import reactor.core.publisher.{ Flux, Mono }

@RestController
@RequestMapping(Array("/users"))
class UserRestController(userService: UserService) {
  @GetMapping(Array("/{id}")) def getUserById(@PathVariable("id") id: String): Mono[User] =
    userService.findById(id)

  @GetMapping(Array("/")) def getAllUsers(): Flux[User] =
    userService.findAll()

  @PostMapping(Array("/")) def saverUser(@RequestBody user: User): Mono[User] = {
    userService.save(user)
    Mono.just(user)
  }

  @PutMapping(Array("/{id}")) def updateUser(@PathVariable("id") id: String, @RequestBody user: User): Mono[User] = {
    user.id = id
    userService.save(user)
    Mono.just(user)
  }

  @DeleteMapping(Array("/{id}")) def deleteUser(@PathVariable("id") id: String): Unit =
    userService.deleteById(id)
}

                    
                
            

UserRestController.scala

7. Spring Boot Context Configuration

This is the main component that starts our Spring Context and our Cassandra Reactive Client

                

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}
                                 
                
            

Application.java

                

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class SpringHibernateCrudApplication

fun main(args: Array) {
    SpringApplication.run(SpringHibernateCrudApplication::class.java, *args)
}

                    
                
            

Application.kt

                
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class Application

object Application extends App{
  SpringApplication.run(classOf[Application])
}

                    
                
            

Application.scala

Database connection parameters are defined in the src/main/resources/application.properties
  • spring.data.cassandra.keyspace-name - defines the Cassandra keyspace where your tables are located
  • spring.data.cassandra.contact-points - defines the Cassandra the hosts that will be conecting to
  • spring.data.cassandra.port - defines the Cassandra Port

8. Source Code

Checkout the whole source code for the following programming languages: