Learnitweb

Building a Reactive Spring Boot Application with PostgreSQL and Performance Testing

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

  1. Go to Spring Initializr: https://start.spring.io/
  2. Select:
    • Project: Maven
    • Language: Java
    • Spring Boot: Latest stable version
    • Packaging: Jar
    • Java Version: 17+ (or your preferred version)
  3. Dependencies:
    • Spring Reactive Web
    • R2DBC (Reactive Relational Database Connectivity)
    • PostgreSQL R2DBC Driver
    • Spring Data R2DBC
  4. Fill in your group and artifact:
    • Group: com.example
    • Artifact: reactive-performance
  5. 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

  1. Install Gatling: https://gatling.io/open-source
  2. Scenario: Simulate multiple users performing GET and PUT requests.
  3. Baseline Test:
    • Run performance tests without any caching or optimization.
  4. 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.