spring spring reactive docker PostgreSQL webflux
Hands on Reactive Spring with Redis Cache and Docker support The concept of reactive programming enables more responsive and scalable programmes by handling asynchronous data streams.
Elattar Saad Mon, 28th August 2023 4 min readWritten by
Saad Elattar Software engineer passionate about building scalable systems, exploring new technologies, and sharing knowledge through writing.
Get in touch 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:
Continuing with the movie theme from previous articles, we start by developing our data model.
Note: Although I’m using records, you are still welcome to utilise classes if you’d like.
Next, let’s build our repository:
The Long type is the type of the movie’s primary key type.
Now, let’s create our service:
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:
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.
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:
Redis configuration:
we need a reactive Redis template to persist our movies:
Postgres database configuration:
In the resource folder, we create a schema.sql file.
The init script is used at the startup in order to create a table for our movies.
Docker compose configuration:
In our compose.yaml file, we define our databases’ services.
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.
spring :
sql :
init :
schema-locations : classpath:schema.sql
mode : always
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 :
import org . springframework . data . relational . core . mapping . Table ;
@ Table (" movies ")
public record Movie ( Long id , String title ) {}
import org . springframework . data . repository . reactive . ReactiveCrudRepository ;
public interface MovieRepository extends ReactiveCrudRepository < Movie , Long > {}
import org . springframework . data . redis . core . ReactiveRedisTemplate ;
import org . springframework . stereotype . Service ;
import reactor . core . publisher . Flux ;
import reactor . core . publisher . Mono
import org . springframework . http . HttpStatus ;
import org . springframework . web . bind . annotation . * ;
import reactor . core . publisher . Flux ;
import reactor . core . publisher . Mono
@ 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 ")
);
import org . springframework . context . annotation . Bean ;
import org . springframework . context . annotation . Configuration ;
import org . springframework . data . redis . connection . ReactiveRedisConnectionFactory ;
import org .
< dependencies >
< dependency >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-starter-data-r2dbc </ artifactId >
</ dependency >
< dependency >
< groupId > org.springframework.boot </ groupId >
;
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.
. set (" movie: " + movie . id (), movie )
// Fetching the movies from the updated cache.
. thenMany ( reactiveRedisTemplate
. flatMap ( key -> reactiveRedisTemplate . opsForValue (). get ( key ))
;
@ RequestMapping (" /movies ")
public record MovieController ( MovieService service ) {
public Flux < Movie > findAll () {
return service . findAll ();
@ ResponseStatus ( HttpStatus . CREATED )
public Mono < Movie > save (@ RequestBody Movie movie ) {
return service . save ( movie );
return args -> repository . deleteAll ()
. thenMany ( repository . saveAll ( movies ))
. subscribe ( System . out :: println );
springframework
.
data
.
redis
.
core
.
ReactiveRedisTemplate
;
import org . springframework . data . redis . serializer . Jackson2JsonRedisSerializer ;
import org . springframework . data . redis . serializer . RedisSerializationContext ;
public class RedisConfiguration {
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 );
< artifactId > spring-boot-starter-data-redis-reactive </ artifactId >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-starter-webflux </ artifactId >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-docker-compose </ artifactId >
< optional > true </ optional >
< groupId > org.postgresql </ groupId >
< artifactId > postgresql </ artifactId >
< groupId > org.postgresql </ groupId >
< artifactId > r2dbc-postgresql </ artifactId >
< groupId > org.springframework.boot </ groupId >
< artifactId > spring-boot-starter-test </ artifactId >
< groupId > io.projectreactor </ groupId >
< artifactId > reactor-test </ artifactId >
CREATE TABLE IF NOT EXISTS movies (
id SERIAL PRIMARY KEY ,
title VARCHAR ( 30 ) NOT NULL
);
< 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 >
import org.springframework.data.relational.core.mapping.Table ;
@ Table ( "movies" )
public record Movie ( Long id , String title) {}
import org.springframework.data.repository.reactive.ReactiveCrudRepository ;
public interface MovieRepository extends ReactiveCrudRepository < Movie , Long > {}
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))
)
);
}
}
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);
}
}
@ 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);
}
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);
}
}
CREATE TABLE IF NOT EXISTS movies (
id SERIAL PRIMARY KEY ,
title VARCHAR ( 30 ) NOT NULL
);
spring :
sql :
init :
schema-locations : classpath:schema.sql
mode : always
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 :