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
- 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