byandrev's blog

5 min read

Your first reactive application with Spring WebFlux

The project

Full code is on GitHub: spring-webflux-demo. It's Spring Boot 4.1.1, Gradle, Java 17, and PostgreSQL through R2DBC.

Project setup
Project setup

Here are the dependencies used:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    runtimeOnly 'org.postgresql:r2dbc-postgresql'
}

Postgres in a container

Postgres in Docker so I don't need to install it locally:

yaml
services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "${POSTGRES_PORT:-5432}:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      retries: 5
 
volumes:
  pgdata:

Connecting with R2DBC instead of JDBC

application.yml points Spring at that container:

yaml
spring:
  application:
    name: spring-reactive-demo
  r2dbc:
    url: r2dbc:postgresql://localhost:${POSTGRES_PORT:5432}/${POSTGRES_DB:demo}
    username: ${POSTGRES_USER:demo}
    password: ${POSTGRES_PASSWORD:demo}

R2DBC stands for Reactive Relational Database Connectivity. It's a different driver spec than JDBC.

JDBC blocks a thread while it waits for the database to answer. R2DBC doesn't. A query returns a Mono or a Flux right away, and the thread is free to do other work until data actually arrives.

The model and the repository

The domain is one record:

java
@Table("transactions")
public record Transaction(
		@Id Long id,
		String account,
		String description,
		BigDecimal amount,
		Instant createdAt) {
}
 

The repository is one line. R2dbcRepository gives you Flux<Transaction> and Mono<Transaction> for free, no query methods needed for this demo:

java
import org.springframework.data.r2dbc.repository.R2dbcRepository;
 
public interface TransactionRepository extends R2dbcRepository<Transaction, Long> {
}

The service just forwards the call. I added it so the controllers don't talk to the repository directly:

java
@Service
public class TransactionService {
 
	private final TransactionRepository repository;
 
	public TransactionService(TransactionRepository repository) {
		this.repository = repository;
	}
 
	public Flux<Transaction> findAll() {
		return repository.findAll();
	}
}

Two ways to expose the same stream

The JSON endpoint returns the Flux directly, as server-sent events:

java
@RestController
@RequestMapping("/api/transactions")
public class TransactionRestController {
	private final TransactionService service;
 
	private static final Duration DEMO_DELAY = Duration.ofMillis(500);
 
	public TransactionRestController(TransactionService service) {
		this.service = service;
	}
 
	@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
	public Flux<Transaction> list() {
		return service.findAll().delayElements(DEMO_DELAY);
	}
}

delayElements is not something you'd ship. Postgres on localhost answers in milliseconds. Without the delay you wouldn't see anything stream, just ten rows landing at once. It's there so the demo is visible.

The HTML version does the same thing through Thymeleaf:

java
@Controller
public class TransactionViewController {
    private final TransactionService service;
 
    public TransactionViewController(TransactionService service) {
        this.service = service;
    }
 
    private static final Duration DEMO_DELAY = Duration.ofMillis(500);
 
    @GetMapping("/transactions")
    public String list(Model model) {
        Flux<Transaction> transactionsFlux = service.findAll().delayElements(DEMO_DELAY);
	ReactiveDataDriverContextVariable transactions = new ReactiveDataDriverContextVariable(transactionsFlux, 1);
 
	model.addAttribute("transactions", transactions);
 
	return "transactions";
    }
}

ReactiveDataDriverContextVariable tells Thymeleaf: don't wait for the whole Flux to finish, render the template chunk by chunk as each Transaction arrives. The 1 is the buffer size, how many elements Thymeleaf waits for before flushing a chunk to the response. I set it to 1 so every row shows up the moment it's ready.

Testing without touching the database

The tests mock the repository and drive the Flux with StepVerifier, so nothing hits Postgres:

java
@Test
void findAllEmitsEveryTransaction() {
    Transaction deposit = new Transaction(1L, "ES01", "Depósito", new BigDecimal("100.00"), Instant.EPOCH);
    Transaction withdrawal = new Transaction(2L, "ES01", "Retiro", new BigDecimal("-40.50"), Instant.EPOCH);
		
    when(repository.findAll()).thenReturn(Flux.just(deposit, withdrawal));
 
    StepVerifier.create(service.findAll())
        .expectNext(deposit, withdrawal)
        .verifyComplete();
}
 
@Test
void findAllCompletesWhenThereAreNoTransactions() {
    when(repository.findAll()).thenReturn(Flux.empty());
    StepVerifier.create(service.findAll()).verifyComplete();
}

StepVerifier subscribes to the Flux and checks what it emits step by step, instead of collecting everything into a list first. That matters here: a bug in the order of emission, or a Flux that never completes, would slip past a plain assertion on a collected list.

Full code: spring-webflux-demo.

Frequently asked questions

What is R2DBC used for?

R2DBC (Reactive Relational Database Connectivity) lets you talk to a relational database like PostgreSQL without blocking a thread while you wait for a response. A query returns a Mono or a Flux, and the calling thread stays free until data actually arrives. JDBC, the standard alternative, blocks that thread.

Can you use Spring WebFlux and Spring MVC in the same application?

You can put both starters on the classpath, but Spring's own docs recommend against it. When both are present, Spring Boot boots as a regular Servlet application on Tomcat, unless you explicitly set spring.main.web-application-type: reactive. Spring MVC controllers can still return reactive types like Flux, which is why streaming keeps working even without the reactive runtime.

How do you stream data from a Spring Boot endpoint?

Return a Flux<T> from a @RestController method with produces = MediaType.TEXT_EVENT_STREAM_VALUE. Spring sends each element as server-sent events as soon as it's emitted, no extra library needed.

Comments