Building Reactive CRUD APIs with Spring Boot, R2DBC, and PostgreSQL
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.
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:
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.
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.
You can find the complete source code for this project on GitHub.