Reactive REST API with Spring WebFlux and Spring Data Cassandra

In this post, we’ll implement a sample REST API that uses Spring Webflux with Spring Data Cassandra.

Prerequisites

This is the list of all the prerequisites:

Overview

What is Apache Cassandra?

Apache Cassandra is an open source distributed 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 robust support for clusters spanning multiple datacenters, with asynchronous masterless replication allowing low latency operations for all clients.

Cassandra keyspaces

A keyspace is the top-level database object that controls the replication for the object it contains at each datacenter in the cluster. Keyspaces contain tables, materialized views and user-defined types, functions and aggregates. Typically, a cluster has one keyspace per application. Since replication is controlled on a per-keyspace basis, store data with different replication requirements (at the same datacenter) in different keyspaces. Keyspaces are not a significant map layer within the data model.

Our cassandra ddl schema:

-- Create a keyspace
CREATE KEYSPACE IF NOT EXISTS keyspaceboottech
    WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }
    AND DURABLE_WRITES = true;

USE keyspaceboottech;


-- Create a tables

CREATE TABLE IF NOT EXISTS author(
                       id uuid PRIMARY KEY,
                       lastname text,
                       firstname text
);

CREATE TABLE IF NOT EXISTS book(
                     id uuid PRIMARY KEY,
                     title text,
                     isbn text,
                     description text,
                     language text,
                     publication date,
                     page int,
                     price double
);

Cassandra Setup with Docker

To start our Cassandra integration with Spring reactive application, we need to start a Cassandra database.

For this, we will use Docker containers. We will create a docker-compose file containing all the instructions to run the Cassandra DB instance in standalone mode.

version: '3.8'
services:
  cassandra:
    image: cassandra:latest
    environment:
      - "MAX_HEAP_SIZE=256M"
      - "HEAP_NEWSIZE=128M"
    volumes:
      - ./apps/cassandra:/var/lib/cassandra
    ports:
      - "7000:7000"
      - "7001:7001"
      - "7199:7199"
      - "9042:9042"
      - "9160:9160"
  cassandra-load-keyspace:
    container_name: cassandra-load-keyspace
    image: cassandra:latest
    depends_on:
      - cassandra
    volumes:
      - ./ddl-schema.cql:/schema.cql
    command: /bin/bash -c "sleep 60 && echo loading cassandra keyspace && cqlsh cassandra -f /schema.cql"

Once you’ve created this yml, open your CLI and run the following command:

docker-compose up -d

This command creates a Cassandra database instance, creates a keyspace, and initializes the database schema from ddl-schema.cql file.

Getting Started

We will start by creating a simple Spring reactive project from start.spring.io, with the following dependencies: Spring Reactive Web, Spring Data Reactive for Apache Cassandra, Lombok, and Validation.

Below is the build.gradle project:

plugins {
    id 'org.springframework.boot' version '2.7.0'
    id 'io.spring.dependency-management' version '1.0.12.RELEASE'
    id 'java'
}

group = 'com.boottech'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/milestone' }
    maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-cassandra-reactive'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

Project Structure

Our project structure will look like this:

Configure Spring Data Cassandra

The main step is to add cassandra’s connection settings in the application.yml file:

spring:
  data:
    cassandra:
      contact-points: datacenter1
      port: 9042
      keyspace-name: keyspaceboottech
      username: root
      password: qwerty

contact-points and keyspace-name are the required fields.

You can also set below configuration using Java configuration. If you want to use Java configuration, use the @EnableCassandraRepositories annotation. The annotation carries the same attributes as the namespace element. If no base package is configured, the infrastructure scans the package of the annotated configuration class.

@Configuration
@EnableCassandraRepositories
class ApplicationConfig extends AbstractReactiveCassandraConfiguration {

  @Override
  protected String getKeyspaceName() {
    return "keyspaceboottech";
  }

  public String[] getEntityBasePackages() {
    return new String[] { "com.boottech.springwebfluxcassandra.domain" };
  }
}

CRUD API

Photo by Braden Collum on Unsplash

To start, we need to create a Bookclass to present a table in Cassandra.

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table("book")
public class Book {
    @PrimaryKey
    private UUID id;
    private String title;
    private int page;
    private String isbn;
    private String description;

    private String language;

    private LocalDate publication;
    private double price;
}

– @Table identifies this model to be persisted to Cassandra as book table.
– @PrimaryKey specifies the primary key field of this entity.

Create Book Repository interface

@Repository
public interface BookRepository extends ReactiveCassandraRepository<Book, UUID> {
}

We will use the basic ReactiveCassandraRepository interface which extends ReactiveCrudRepository to offer some variations of the insert method.

Service Layer

The service layer implementation class (BookServiceImpl) will be injected by the BookRepository.

@Slf4j
@Service
public class BookServiceImpl implements BookService {

    private final BookRepository repository;

    public BookServiceImpl(BookRepository repository) {
        this.repository = repository;
    }

    @Override
    public Flux<Book> getAll() {
        return  repository.findAll();
    }

    @Override
    public Mono<Book> add(Book book) {
        book.setId(UUID.randomUUID());
        log.info("addBook : {} " , book );
        return repository.save(book)
            .log();
    }

    @Override
    public Mono<Book> getById(String id) {
        return repository.findById(UUID.fromString(id))
            .switchIfEmpty(Mono.error(new DataNotFoundException("Book id not found")));
    }

    @Override
    public Mono<Book> update(Book book, String id) {
        return repository.findById(UUID.fromString(id))
            .flatMap(book1 -> {
                book1.setTitle(book.getTitle());
                book1.setIsbn(book.getIsbn());
                book1.setDescription(book.getDescription());
                book1.setLanguage(book.getLanguage());
                book1.setPage(book.getPage());
                book1.setPrice(book.getPrice());
                book1.setPublication(book.getPublication());
                return repository.save(book1);
            });
    }

    @Override
    public Mono<Void> deleteById(String id) {
        return repository.deleteById(UUID.fromString(id));
    }
}

Create Book Controller

@RestController
@RequestMapping("/api")
@Slf4j
public class BookController {

    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping("/book")
    public Flux<Book> getAllBooks() {
        return bookService.getAll();
    }

    @GetMapping("/book/{id}")
    public Mono<ResponseEntity<Book>> getBookById(@PathVariable("id") String id) {

        return bookService.getById(id)
            .map(book1 -> ResponseEntity.ok()
                .body(book1))
            .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()))
            .log();
    }

    @PostMapping("/book")
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Book> addBook(@RequestBody @Valid Book book) {
        return bookService.add(book);

    }

    @PutMapping("/book/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Mono<ResponseEntity<Book>> updateBook(@RequestBody Book book, @PathVariable String id) {

        var updatedBookMono =  bookService.update(book, id);
        return updatedBookMono
            .map(book1 -> ResponseEntity.ok()
                .body(book1))
            .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));

    }

    @DeleteMapping("/book/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public Mono<Void> deleteBookById(@PathVariable String id){
        return bookService.deleteById(id);
    }

}

Test the application

Now we can run our application and test it.

  • Create Book

In cqlsh console we can check cassandra database:

select * from keyspaceboottech.book;
  • Get All Book
  • Update book

Conclusion

We’ve built a Rest CRUD API with Spring WebFlux and Spring Data Cassandra.

The complete source code is available on GitHub.

References

👉 Link to Medium blog

Related Posts