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/1
with body:
{ "description": "Updated Product", "price": 150.00 }
- Create product:
POST http://localhost:8080/products
with 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.