Skip to main content

Building Reactive CRUD APIs with Spring Boot, R2DBC, and PostgreSQL

· 7 min read
Huseyin BABAL
Software Developer

Introduction

In the world of modern web applications, responsiveness and scalability are paramount. Enter R2DBC (Reactive Relational Database Connectivity) - a game-changing paradigm that brings the power of reactive programming to your database operations. In this article, we'll explore how to build a high-performance, non-blocking CRUD API using Spring Boot 3, R2DBC, and PostgreSQL.

What is R2DBC?

R2DBC is a specification that defines a reactive API for relational databases. Unlike traditional JDBC, which uses a blocking approach, R2DBC enables non-blocking database operations, allowing your application to handle more concurrent connections with fewer threads. This makes it an excellent choice for building reactive applications that can scale efficiently.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Guide to Creating Project

Project Initialization and Dependencies

We will be using Spring Boot and PostgreSQL to build a todo application. You can initialize a spring boot project by using Spring Boot CLI. Once installed, you can use following command to initialize a project with required dependencies.

spring init \
--dependencies=web,data-jpa,postgresql,data-r2dbc,webflux \
--type=maven-project \
--javaVersion=21 \
spring-reactive-api

Line 2: web for implementing REST endpoints, data-r2dbc for database persistence, webflux for reactive endpoints, and postgresql for PostgreSQL driver.

Line 3: --type=maven-project for creating a Maven project.

Line 4: --javaVersion=21 we will use Java 21 to compile and run project.

Now that we initialized the project, go to the folder spring-reative-api and open it with your favourite IDE.

Implementing Entity and Repository

We have only one entity here, Car, which will be used to store our cars. Let's create a new entity called Car as follows.

@Data
@Table
@AllArgsConstructor
@NoArgsConstructor
class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String make;
private String model;
private Integer year;
private String color;
}

In order to manage Car entity in database, we will use following repository interface.

interface CarRepository extends R2dbcRepository<Car, Long> {}

Be sure that we are using R2dbcRepository instead of JpaRepository.

Application Configuration

This section contains application level configurations such as the application name and r2dbc as shown below:

application.yaml
spring:
application:
name: spring-reactive-api
r2dbc:
url: <connection-string-from-rapidapp|or your own managed postgres url>
username: <username>
password: <password>
sql:
init: always

Line 5: Connection URL for the PostgreSQL database. You can obtain this from Rapidapp or your own managed PostgreSQL service. It should have a format like jdbc:postgresql://<host>:<port>/<database>?sslmode=require.

Implementing Services for CRUD Operations

We will create a service class to handle CRUD operations for the Car entity. This class will interact with the repository

@Service
class CarService {
private final CarRepository carRepository;

public CarService(CarRepository carRepository) {
this.carRepository = carRepository;
}

public Flux<Car> getAllCars() {
return carRepository.findAll();
}

public Mono<Car> getCarById(Long id) {
return carRepository.findById(id);
}

public Mono<Car> createCar(Car car) {
return carRepository.save(car);
}

public Mono<Car> updateCar(Long id, Car car) {
return carRepository.findById(id)
.flatMap(existingCar -> {
car.setId(id);
return carRepository.save(car);
});
}

public Mono<Void> deleteCar(Long id) {
return carRepository.deleteById(id);
}
}

Most probably you are familiar with business service classes, but this case we have Mono and Flux keywords, let's explain them.

Understanding Reactive Types: Mono and Flux

When working with reactive programming in Spring, you'll encounter two fundamental types: Mono and Flux. These are implementations of the Reactive Streams specification and are crucial for handling non-blocking operations.

Mono

A Mono<T> represents a stream that emits at most one item and then completes (successfully or with an error). Think of it as an asynchronous equivalent to:

  • A single value
  • No value (empty)
  • An error
// Example of different Mono scenarios
Mono<Car> carMono = carRepository.findById(1L); // 0 or 1 car
Mono<Car> newCarMono = carRepository.save(newCar); // Created car
Mono<Void> deleteMono = carRepository.deleteById(1L); // No return value

Flux

A Flux<T> represents a stream that can emit 0 to N items and then completes (successfully or with an error). It's ideal for handling:

  • Multiple values
  • Streams of data
  • Continuous updates
// Example of different Flux scenarios
Flux<Car> allCars = carRepository.findAll(); // 0 to N cars
Flux<Car> toyotaCars = carRepository.findByMake("Toyota"); // Filtered stream

How Spring Handles Non-Blocking Requests

Traditional (Blocking) Approach:

Client Request → Thread assigned → Database Operation → Thread waits → Response → Thread released

Reactive (Non-Blocking) Approach:

Client Request → Event Loop registers callback → Thread released →
Database Operation (async) → Callback triggered → Response

Event Loop Model

Spring WebFlux uses an event loop model powered by Project Reactor:

  • Request Acceptance: When a request arrives, it's accepted by a small number of threads (typically one per CPU core).

  • Non-Blocking Processing: Instead of waiting for operations to complete, the thread registers callbacks and moves on to handle other requests.

@GetMapping("/{id}")
public Mono<Car> getCarById(@PathVariable Long id) {
return carService.getCarById(id);
// Thread doesn't wait here! It's free to handle other requests
}
  • Asynchronous Execution: Database operations occur asynchronously:
public Mono<Car> getCarById(Long id) {
return carRepository.findById(id)
.map(car -> {
// This runs when data is available, not immediately
log.info("Car found: {}", car);
return car;
})
.defaultIfEmpty(/* handle not found case */);
}

Now that we have more insights about Mono and Flux, let's continue with our controller class.

Implementing Controller for REST Endpoints

We will create a controller class to handle REST endpoints for the Car entity. This class will interact with the service class.

@RestController
@RequestMapping("/api/cars")
public class CarController {
private final CarService carService;

public CarController(CarService carService) {
this.carService = carService;
}

@GetMapping
public Flux<Car> getAllCars() {
return carService.getAllCars();
}

@GetMapping("/{id}")
public Mono<Car> getCarById(@PathVariable Long id) {
return carService.getCarById(id);
}

@PostMapping
public Mono<Car> createCar(@RequestBody Car car) {
return carService.createCar(car);
}

@PutMapping("/{id}")
public Mono<Car> updateCar(@PathVariable Long id, @RequestBody Car car) {
return carService.updateCar(id, car);
}

@DeleteMapping("/{id}")
public Mono<Void> deleteCar(@PathVariable Long id) {
return carService.deleteCar(id);
}
}

Schema Preparation

Before running the application, you need to create a table in your PostgreSQL database to store the Car entity. You can use the following schema.sql in the resources folder to create the table.

src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS car (
id SERIAL PRIMARY KEY,
make VARCHAR(255) NOT NULL,
model VARCHAR(255) NOT NULL,
year INTEGER NOT NULL,
color VARCHAR(255)
);

That's it! You have successfully created a Spring Reactive API with PostgreSQL as the database. You can now run the application as follows;

mvn spring-boot:run

Demo

Create a new car

curl -X POST http://localhost:8080/api/cars \
-H "Content-Type: application/json" \
-d '{"make":"Toyota","model":"Camry","year":2023,"color":"Silver"}'

Get all cars

curl http://localhost:8080/api/cars

Get a car by id

curl http://localhost:8080/api/cars/1

Update a car

curl -X PUT http://localhost:8080/api/cars/1 \
-H "Content-Type: application/json" \
-d '{"make":"Toyota","model":"Camry","year":2023,"color":"Blue"}'

Delete a car

curl -X DELETE http://localhost:8080/api/cars/1

Key Benefits of Using R2DBC

  • Non-blocking Operations: R2DBC enables fully non-blocking database interactions, improving application responsiveness.
  • Scalability: Handle more concurrent connections with fewer threads.
  • Backpressure Support: Built-in mechanisms to handle scenarios where producers are faster than consumers.
  • Integration with Reactive Streams: Seamlessly works with Spring WebFlux and other reactive components.

Performance Considerations

While R2DBC offers significant advantages for reactive applications, it's important to note that it may not always outperform traditional JDBC in terms of raw throughput. The real benefits come in scenarios with:

  • High concurrency
  • Long-running queries
  • Applications that already use reactive programming models

Conclusion

R2DBC with Spring Boot 3 provides a powerful foundation for building reactive, scalable applications. By leveraging non-blocking database operations, you can create more responsive and resource-efficient services. As the R2DBC ecosystem continues to grow, it's becoming an increasingly attractive option for modern application development.

tip

You can find the complete source code for this project on GitHub.