In this tutorial, we will create a simple Spring Boot application to manage products, store data in PostgreSQL using R2DBC, and verify performance using Gatling. This approach is suitable for real-world applications where you want reactive programming and performance insights.
1. Project Setup
- Go to Spring Initializr: https://start.spring.io/
- Select:
- Project: Maven
- Language: Java
- Spring Boot: Latest stable version
- Packaging: Jar
- Java Version: 17+ (or your preferred version)
- Dependencies:
- Spring Reactive Web
- R2DBC (Reactive Relational Database Connectivity)
- PostgreSQL R2DBC Driver
- Spring Data R2DBC
- Fill in your group and artifact:
- Group:
com.example - Artifact:
reactive-performance
- Group:
- Generate and import the project into your IDE.
2. Database Setup: PostgreSQL with Docker
We will run PostgreSQL in a Docker container for easy setup.
docker run -d \ --name postgres-reactive \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ -p 5432:5432 \ -v pgdata:/var/lib/postgresql/data \ postgres:15
Optional: You can also use pgAdmin for database administration:
docker run -d \ --name pgadmin \ -p 8080:80 \ -e PGADMIN_DEFAULT_EMAIL=admin@admin.com \ -e PGADMIN_DEFAULT_PASSWORD=admin \ dpage/pgadmin4
3. Database Schema
Create a schema.sql file to define the product table:
DROP TABLE IF EXISTS product;
CREATE TABLE product (
id SERIAL PRIMARY KEY,
description VARCHAR(255),
price DECIMAL(10,2)
);
4. Entity Definition
package com.example.reactiveperformance.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import java.math.BigDecimal;
@Table("product")
public class Product {
@Id
private Integer id;
private String description;
private BigDecimal price;
public Product() {}
public Product(String description, BigDecimal price) {
this.description = description;
this.price = price;
}
// Getters and setters
}
5. Repository Layer
Reactive repository for PostgreSQL using R2DBC:
package com.example.reactiveperformance.repository;
import com.example.reactiveperformance.entity.Product;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends ReactiveCrudRepository<Product, Integer> {
}
6. Service Layer
Reactive service with CRUD operations:
package com.example.reactiveperformance.service;
import com.example.reactiveperformance.entity.Product;
import com.example.reactiveperformance.repository.ProductRepository;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
public Mono<Product> getProductById(Integer id) {
return repository.findById(id);
}
public Flux<Product> getAllProducts() {
return repository.findAll();
}
public Mono<Product> updateProduct(Integer id, Product updatedProduct) {
return repository.findById(id)
.flatMap(product -> {
product.setDescription(updatedProduct.getDescription());
product.setPrice(updatedProduct.getPrice());
return repository.save(product);
});
}
public Mono<Product> createProduct(Product product) {
return repository.save(product);
}
}
7. Controller Layer
Expose REST endpoints for CRUD operations:
package com.example.reactiveperformance.controller;
import com.example.reactiveperformance.entity.Product;
import com.example.reactiveperformance.service.ProductService;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping
public Flux<Product> getAllProducts() {
return service.getAllProducts();
}
@GetMapping("/{id}")
public Mono<Product> getProduct(@PathVariable Integer id) {
return service.getProductById(id);
}
@PutMapping("/{id}")
public Mono<Product> updateProduct(@PathVariable Integer id, @RequestBody Product product) {
return service.updateProduct(id, product);
}
@PostMapping
public Mono<Product> createProduct(@RequestBody Product product) {
return service.createProduct(product);
}
}
8. Data Initialization
Automatically populate the database on startup using CommandLineRunner:
package com.example.reactiveperformance.service;
import com.example.reactiveperformance.entity.Product;
import com.example.reactiveperformance.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import java.math.BigDecimal;
import java.util.stream.IntStream;
@Component
public class DataSetupService implements CommandLineRunner {
private final ProductRepository repository;
private final DatabaseClient client;
public DataSetupService(ProductRepository repository, DatabaseClient client) {
this.repository = repository;
this.client = client;
}
@Override
public void run(String... args) throws Exception {
// Drop and recreate table using schema.sql
client.sql(new ClassPathResource("schema.sql").getInputStream().readAllBytes())
.fetch()
.rowsUpdated()
.subscribe();
// Insert sample products
Flux<Product> products = Flux.fromIterable(
IntStream.rangeClosed(1, 2000)
.mapToObj(i -> new Product("Product " + i, BigDecimal.valueOf(100 + i)))
.toList()
);
repository.saveAll(products).subscribe();
}
}
9. Testing the API
Use Postman or any REST client to test:
- GET all products:
GET http://localhost:8080/products - GET by ID:
GET http://localhost:8080/products/1 - Update product:
PUT http://localhost:8080/products/1with body:
{
"description": "Updated Product",
"price": 150.00
}
- Create product:
POST http://localhost:8080/productswith body:
{
"description": "New Product",
"price": 200.00
}
10. Performance Testing with Gatling
- Install Gatling: https://gatling.io/open-source
- Scenario: Simulate multiple users performing GET and PUT requests.
- Baseline Test:
- Run performance tests without any caching or optimization.
- Reactive R2DBC Test:
- Add reactive programming support.
- Run the same scenario and compare results.
Metrics to compare:
- Response time
- Throughput (requests/sec)
- CPU and memory usage
Summary
- We built a reactive Spring Boot application with PostgreSQL using R2DBC.
- We exposed CRUD endpoints for products.
- Added data initialization to populate the database automatically.
- Set up performance testing using Gatling to benchmark reactive vs non-reactive behavior.
This setup provides a foundation for real-world performance testing, reactive programming, and efficient database interaction.
