Hands on Reactive Spring with Redis Cache and Docker support

ELATTAR Saad
5 min readAug 28, 2023

--

The concept of reactive programming enables more responsive and scalable programmes by handling asynchronous data streams. It emphasises on representing data flows as ongoing streams of events so that systems may respond and adjust in real time to shifting circumstances. Reactive programming provides a potent tool for handling complicated interactions and upholding fluid user experiences by utilising the ideas of event-driven and declarative programming. Its method is particularly useful in situations where event-driven structures and effective handling of asynchronous activities are critical, such as in contemporary web applications and real-time data processing systems.

Project Reactor by Spring is a completely non-blocking foundation with built-in back-pressure support. It serves as the structural core of the Spring ecosystem’s reactive stack and is included into initiatives like Spring WebFlux, Spring Data, and Spring Cloud Gateway.

While attempting to maintain functional simplicity, this demonstration will serve as a proof-of-concept (POC) for creating reactive REST endpoints supported by a PostgreSQL database and using Redis as a cache, all with the support of Docker containers.

In this app we will be using:

  • Java version 17.
  • Spring Boot version 3.1.2.

And the following dependencies:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Continuing the movie theme from previous articles, we start by developing our data model.

import org.springframework.data.relational.core.mapping.Table;

@Table("movies")
public record Movie(Long id, String title) {}

Note: Although I’m using records, you are still welcome to utilise classes if you’d like.

Next, let’s build our reactive repository:

import org.springframework.data.repository.reactive.ReactiveCrudRepository;

public interface MovieRepository extends ReactiveCrudRepository<Movie, Long> {}

Note: the Long type is the type of the movie’s primary key type.

Now, let’s create our service:

import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class MovieService {

private final MovieRepository movieRepository;
private final ReactiveRedisTemplate<String, Movie> reactiveRedisTemplate;

public MovieService(MovieRepository movieRepository, ReactiveRedisTemplate<String, Movie> reactiveRedisTemplate) {
this.movieRepository = movieRepository;
this.reactiveRedisTemplate = reactiveRedisTemplate;
}

public Mono<Movie> save(Movie movie) {
return movieRepository.save(movie);
}

public Flux<Movie> findAll() {
return reactiveRedisTemplate.keys("movie:*")
// Fetching cached movies.
.flatMap(key -> reactiveRedisTemplate.opsForValue().get(key))
// If cache is empty, fetch the database for movies
.switchIfEmpty(movieRepository.findAll()
// Persisting the fetched movies in the cache.
.flatMap(movie ->
reactiveRedisTemplate
.opsForValue()
.set("movie:" + movie.id(), movie)
)
// Fetching the movies from the updated cache.
.thenMany(reactiveRedisTemplate
.keys("movie:*")
.flatMap(key -> reactiveRedisTemplate.opsForValue().get(key))
)
);
}
}

To better understand how to use the Mono and Flux publishers, I suggest reading this insightful article.

It’s time to create our movie rest controller:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/movies")
public record MovieController(MovieService service) {

@GetMapping
public Flux<Movie> findAll() {
return service.findAll();
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<Movie> save(@RequestBody Movie movie) {
return service.save(movie);
}
}

Note: For simplicity, the DTO, validation, mapping and exception layers are not used.

Then, we need a starting script to persist a set of movies in our database. You can use the script in the main class or any configuration class.

@Bean
public ApplicationRunner saveMovies(MovieRepository repository) {
Flux<Movie> movies = Flux.just(
new Movie(null, "Catch me if you can"),
new Movie(null, "Interstellar"),
new Movie(null, "Fight Club"),
new Movie(null, "Creed"),
new Movie(null, "The Godfather")
);
return args -> repository.deleteAll()
.thenMany(repository.saveAll(movies))
.subscribe(System.out::println);
}

Note: The save operation won’t populate our cache, only the findAll will.

As the next part of this demo, some configurations need to take place:

  1. Redis configuration:

we need a reactive Redis template to persist our movies:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

@Configuration
public class RedisConfiguration {
@Bean
public ReactiveRedisTemplate<String, Movie> reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {
Jackson2JsonRedisSerializer<Movie> serializer = new Jackson2JsonRedisSerializer<>(Movie.class);
RedisSerializationContext.RedisSerializationContextBuilder<String, Movie> builder =RedisSerializationContext.newSerializationContext(new Jackson2JsonRedisSerializer<>(String.class));
RedisSerializationContext<String, Movie> context = builder.value(serializer).build();
return new ReactiveRedisTemplate<>(factory, context);
}
}

2. Postgres database configuration:

In the resource folder, we create a schema.sql file.

CREATE TABLE IF NOT EXISTS movies (
id SERIAL PRIMARY KEY,
title VARCHAR(30) NOT NULL
);

The init script is used at the startup in order to create a table for our movies.

spring:
sql:
init:
schema-locations: classpath:schema.sql
mode: always

3. Docker compose configuration:

In our compose.yaml file, we define our databases’ services.

services:
postgres:
container_name: postgres_local_db
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: moviesDB
ports:
- "5432:5432"
volumes:
- local_dev:/var/lib/postgresql/data
redis:
container_name: redis_local_db
image: "redis/redis-stack:latest"
ports:
- "6379:6379"
- "8001:8001"
environment:
- REDIS_REPLICATION_MODE=master
volumes:
local_dev:

Now that we’ve finished building our reactive cached endpoint, let’s put it to use:

Our cache is empty, let’s run the findAll op to fill it up with movies from the PostgreSQL database:

The findAll is successfully returning the list of persisted movies. Let’s check our cache:

Now that our cache is filled, the next findAll will be based on it.

The Spring’s Docker support is the reason why the databases services are running at the same time with the movie service.

Once the app is terminated, all the related services will be terminated as well.

Finally

This proof of concept acts as a stepping stone, giving an idea of the enormous potential that may be realised by fully using these cutting-edge technologies. Although the present POC has a constrained scope, its goal is to illustrate the fundamental ideas and skills that may be expanded upon and enhanced.

Find me on LinkedIn.

--

--

ELATTAR Saad

Warm regards! I'm Saad, a software engineer. The most enjoyable part of any journey is what you share along the way, so code and document the journey!