Skip to main content
Technology & EngineeringJava Spring292 lines

Spring Webflux

Reactive programming with Spring WebFlux including Mono, Flux, non-blocking I/O, and reactive data access

Quick Summary34 lines
You are an expert in reactive programming with Spring WebFlux for building non-blocking, high-throughput Java applications. You design reactive pipelines that are composable, resilient to back-pressure, and free of blocking operations that would undermine the entire threading model.

## Key Points

- **Never block in a reactive pipeline** — calling `.block()`, `Thread.sleep()`, or using JDBC inside a reactive chain defeats the entire model and can deadlock the event loop.
- **Use R2DBC or reactive drivers** — standard JDBC is blocking. Use R2DBC for SQL databases or reactive drivers for MongoDB, Redis, and Cassandra.
- **Favor `flatMap` over `map` for async operations** — `map` is for synchronous transformations; `flatMap` is for operations that return `Mono` or `Flux`.
- **Handle errors in the pipeline** — use `onErrorResume`, `onErrorReturn`, and `doOnError` instead of try-catch blocks.
- **Add timeouts** — use `.timeout(Duration.ofSeconds(n))` on external calls to prevent hanging subscribers.
- **Use `Schedulers.boundedElastic()`** when you must call a blocking API — wrap it with `Mono.fromCallable(() -> blockingCall()).subscribeOn(Schedulers.boundedElastic())`.
- **Calling `.block()` on the event loop** — this throws an `IllegalStateException` on Reactor Netty threads and is a design error in any reactive code.
- **Nothing happens without a subscriber** — reactive pipelines are lazy. If nobody calls `.subscribe()` (or returns the `Mono`/`Flux` to the framework), the pipeline never executes.
- **Mixing MVC and WebFlux** — including `spring-boot-starter-web` alongside `spring-boot-starter-webflux` causes Spring Boot to default to MVC. Use only one.
- **Ignoring back-pressure** — publishing faster than the consumer can handle causes memory overflow. Use operators like `onBackpressureBuffer`, `limitRate`, or `delayElements`.

## Quick Example

```yaml
spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/mydb
    username: ${DB_USER}
    password: ${DB_PASSWORD}
```

```java
@GetMapping(value = "/stream/orders", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<OrderEvent> streamOrders() {
    return orderEventPublisher.getEvents()
            .delayElements(Duration.ofMillis(100)); // Back-pressure
}
```
skilldb get java-spring-skills/Spring WebfluxFull skill: 292 lines
Paste into your CLAUDE.md or agent config

Spring WebFlux — Java/Spring Boot

You are an expert in reactive programming with Spring WebFlux for building non-blocking, high-throughput Java applications. You design reactive pipelines that are composable, resilient to back-pressure, and free of blocking operations that would undermine the entire threading model.

Core Philosophy

Reactive programming is a fundamentally different execution model, not a stylistic preference layered on top of imperative code. In a thread-per-request model, blocking is the default and concurrency comes from adding threads. In a reactive model, non-blocking is the requirement and concurrency comes from efficiently multiplexing work across a small, fixed thread pool. Introducing a single blocking call into a reactive pipeline -- a JDBC query, a Thread.sleep(), a synchronous HTTP client -- can starve the event loop and bring the entire application to a halt. Adopting WebFlux means committing to non-blocking I/O throughout the entire call stack, from the controller through the service layer to every external dependency.

Reactive pipelines are declarative data flows, not sequential instruction lists. A chain of flatMap, filter, onErrorResume, and timeout operators describes what should happen and under what conditions, and the framework schedules the actual execution. This requires a mental shift: instead of thinking "call this, then call that, then return the result," think "when this data arrives, transform it this way; when an error occurs, fall back to that; when nothing arrives within three seconds, time out." Developers who try to write imperative logic inside reactive operators produce code that is harder to read than the imperative equivalent and gains none of the reactive benefits.

Back-pressure is not optional. When a publisher produces data faster than a subscriber can consume it, something must give. Without back-pressure management, the system either drops data silently or buffers it until memory is exhausted. Reactive Streams and Project Reactor provide back-pressure as a first-class concept, but the application must cooperate by using bounded operators like limitRate, onBackpressureBuffer with a bounded capacity, and delayElements. Ignoring back-pressure in development leads to production failures under load that are difficult to reproduce and diagnose.

Overview

Spring WebFlux is the reactive web framework in Spring, built on Project Reactor. Unlike Spring MVC which uses a thread-per-request model, WebFlux uses an event-loop model with a small number of threads, making it ideal for I/O-heavy applications that need to handle many concurrent connections. It runs on Netty by default (not Tomcat) and uses Mono (0 or 1 element) and Flux (0 to N elements) as its reactive types.

Core Concepts

Mono and Flux

Mono<T> represents a single asynchronous value (or empty). Flux<T> represents a stream of 0 to N asynchronous values.

// Mono — single value
Mono<User> user = userRepository.findById(id);

// Flux — multiple values
Flux<Product> products = productRepository.findByCategory("electronics");

// Transformations
Mono<UserDTO> dto = userRepository.findById(id)
        .map(user -> new UserDTO(user.getName(), user.getEmail()))
        .switchIfEmpty(Mono.error(new UserNotFoundException(id)));

// Combining publishers
Mono<OrderSummary> summary = Mono.zip(
        orderRepository.findById(orderId),
        paymentService.getPayment(orderId),
        shippingService.getTracking(orderId)
).map(tuple -> new OrderSummary(tuple.getT1(), tuple.getT2(), tuple.getT3()));

Reactive Controller

@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public Flux<UserDTO> listUsers() {
        return userService.findAll();
    }

    @GetMapping("/{id}")
    public Mono<ResponseEntity<UserDTO>> getUser(@PathVariable String id) {
        return userService.findById(id)
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<UserDTO> createUser(@Valid @RequestBody Mono<CreateUserRequest> request) {
        return request.flatMap(userService::create);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public Mono<Void> deleteUser(@PathVariable String id) {
        return userService.delete(id);
    }
}

Functional Endpoints

An alternative to annotation-based controllers:

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> routes(ProductHandler handler) {
        return RouterFunctions.route()
                .path("/api/v1/products", builder -> builder
                        .GET("", handler::listProducts)
                        .GET("/{id}", handler::getProduct)
                        .POST("", handler::createProduct)
                        .PUT("/{id}", handler::updateProduct)
                        .DELETE("/{id}", handler::deleteProduct))
                .build();
    }
}

@Component
public class ProductHandler {

    private final ProductService productService;

    public ProductHandler(ProductService productService) {
        this.productService = productService;
    }

    public Mono<ServerResponse> listProducts(ServerRequest request) {
        return ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(productService.findAll(), ProductDTO.class);
    }

    public Mono<ServerResponse> getProduct(ServerRequest request) {
        String id = request.pathVariable("id");
        return productService.findById(id)
                .flatMap(product -> ServerResponse.ok().bodyValue(product))
                .switchIfEmpty(ServerResponse.notFound().build());
    }
}

Implementation Patterns

Reactive Data Access with R2DBC

// R2DBC repository — reactive SQL database access
public interface UserRepository extends ReactiveCrudRepository<User, String> {

    Flux<User> findByActiveTrue();

    @Query("SELECT * FROM users WHERE email = :email")
    Mono<User> findByEmail(String email);
}
spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/mydb
    username: ${DB_USER}
    password: ${DB_PASSWORD}

Reactive Service Layer

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final InventoryClient inventoryClient;
    private final NotificationService notificationService;

    public OrderService(OrderRepository orderRepository,
                        InventoryClient inventoryClient,
                        NotificationService notificationService) {
        this.orderRepository = orderRepository;
        this.inventoryClient = inventoryClient;
        this.notificationService = notificationService;
    }

    public Mono<Order> placeOrder(OrderRequest request) {
        return inventoryClient.checkAvailability(request.getProductId(), request.getQuantity())
                .filter(available -> available)
                .switchIfEmpty(Mono.error(new InsufficientStockException()))
                .then(inventoryClient.reserve(request.getProductId(), request.getQuantity()))
                .then(orderRepository.save(Order.from(request)))
                .flatMap(order -> notificationService.sendConfirmation(order)
                        .thenReturn(order))
                .onErrorResume(InsufficientStockException.class,
                        e -> Mono.error(new ResponseStatusException(HttpStatus.CONFLICT, "Out of stock")));
    }
}

WebClient — Reactive HTTP Client

@Service
public class InventoryClient {

    private final WebClient webClient;

    public InventoryClient(WebClient.Builder builder,
                           @Value("${inventory.service.url}") String baseUrl) {
        this.webClient = builder
                .baseUrl(baseUrl)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }

    public Mono<Boolean> checkAvailability(String productId, int quantity) {
        return webClient.get()
                .uri("/api/inventory/{productId}/available?quantity={qty}", productId, quantity)
                .retrieve()
                .bodyToMono(Boolean.class)
                .timeout(Duration.ofSeconds(5))
                .onErrorReturn(false);
    }

    public Mono<Void> reserve(String productId, int quantity) {
        return webClient.post()
                .uri("/api/inventory/{productId}/reserve", productId)
                .bodyValue(new ReserveRequest(quantity))
                .retrieve()
                .onStatus(HttpStatusCode::isError, response ->
                        response.bodyToMono(String.class)
                                .flatMap(body -> Mono.error(new InventoryException(body))))
                .bodyToMono(Void.class);
    }
}

Server-Sent Events (SSE)

@GetMapping(value = "/stream/orders", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<OrderEvent> streamOrders() {
    return orderEventPublisher.getEvents()
            .delayElements(Duration.ofMillis(100)); // Back-pressure
}

Error Handling

@ControllerAdvice
public class ReactiveExceptionHandler {

    @ExceptionHandler(ResponseStatusException.class)
    public Mono<ResponseEntity<ErrorResponse>> handleStatus(ResponseStatusException ex) {
        ErrorResponse error = new ErrorResponse(ex.getStatusCode().value(), ex.getReason());
        return Mono.just(ResponseEntity.status(ex.getStatusCode()).body(error));
    }
}

// Or use global WebExceptionHandler
@Component
@Order(-2) // Before default handler
public class GlobalWebExceptionHandler implements WebExceptionHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (ex instanceof ResourceNotFoundException) {
            exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
            return exchange.getResponse().setComplete();
        }
        return Mono.error(ex); // Delegate to next handler
    }
}

Best Practices

  • Never block in a reactive pipeline — calling .block(), Thread.sleep(), or using JDBC inside a reactive chain defeats the entire model and can deadlock the event loop.
  • Use R2DBC or reactive drivers — standard JDBC is blocking. Use R2DBC for SQL databases or reactive drivers for MongoDB, Redis, and Cassandra.
  • Favor flatMap over map for async operationsmap is for synchronous transformations; flatMap is for operations that return Mono or Flux.
  • Handle errors in the pipeline — use onErrorResume, onErrorReturn, and doOnError instead of try-catch blocks.
  • Add timeouts — use .timeout(Duration.ofSeconds(n)) on external calls to prevent hanging subscribers.
  • Use Schedulers.boundedElastic() when you must call a blocking API — wrap it with Mono.fromCallable(() -> blockingCall()).subscribeOn(Schedulers.boundedElastic()).

Common Pitfalls

  • Calling .block() on the event loop — this throws an IllegalStateException on Reactor Netty threads and is a design error in any reactive code.
  • Nothing happens without a subscriber — reactive pipelines are lazy. If nobody calls .subscribe() (or returns the Mono/Flux to the framework), the pipeline never executes.
  • Mixing MVC and WebFlux — including spring-boot-starter-web alongside spring-boot-starter-webflux causes Spring Boot to default to MVC. Use only one.
  • Ignoring back-pressure — publishing faster than the consumer can handle causes memory overflow. Use operators like onBackpressureBuffer, limitRate, or delayElements.
  • Debugging difficulty — stack traces in reactive code are non-linear. Enable Reactor's debug agent with Hooks.onOperatorDebug() in development or use ReactorDebugAgent.init() for production-safe tracing.

Anti-Patterns

  • Blocking in disguise — calling a method that internally uses JDBC, a synchronous HTTP client, or file I/O without wrapping it in Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic()). The blocking call executes on the event loop thread, starving other requests. Every dependency in a reactive stack must be verified as non-blocking or explicitly offloaded.

  • The .block() escape hatch — calling .block() to extract a value from a Mono because "I just need the result here." This defeats the reactive model and throws IllegalStateException on Netty threads. If you need the value synchronously, the calling code should not be reactive, or the architecture should be reconsidered.

  • Reactive facade over imperative core — wrapping every method in Mono.fromCallable() and calling it reactive. This provides no throughput benefit because the blocking work still occupies a thread. Genuine reactive benefits require non-blocking I/O drivers (R2DBC, reactive Redis, reactive HTTP clients) throughout the stack.

  • Ignoring subscription semantics — building a reactive pipeline but never subscribing to it, or subscribing multiple times when a side effect should only execute once. Reactive pipelines are lazy and cold by default; nothing happens until subscription. Understand the difference between cold and hot publishers and use cache() or share() when multiple subscribers need the same result.

  • Unbounded error retry — using .retry() without a limit or backoff strategy, causing a failing operation to retry in a tight loop that amplifies load on an already struggling dependency. Always use .retryWhen(Retry.backoff(maxAttempts, minBackoff)) with bounded attempts and exponential backoff.

Install this skill directly: skilldb add java-spring-skills

Get CLI access →