spring-projects/spring-framework
Reactive stack
Active contributors: rstoyanchev, Sébastien Deleuze, Juergen Hoeller, Brian Clozel
The reactive stack is the non-blocking complement to Spring's traditional Servlet-based web framework. It centers on Reactor types (Mono, Flux) and operates end-to-end without thread-per-request semantics. This page walks through what's reactive in the framework and how the pieces work together.
What's reactive
| Layer | Module | Reactive entry point |
|---|---|---|
| Web server | spring-webflux | DispatcherHandler, WebHandler, WebFilter |
| HTTP client | spring-webflux (WebClient) |
WebClient |
| WebSocket | spring-webflux / spring-websocket | WebSocketHandler |
| Database | spring-r2dbc | DatabaseClient |
| Messaging | spring-messaging | ReactiveMessageHandler, RSocket |
| Transactions | spring-tx | ReactiveTransactionManager, TransactionalOperator |
| Validation | spring-context | Validator is sync; method-level @Validated works on reactive returns via the ReactiveAdapterRegistry |
Reactor as the lingua franca
Spring's reactive APIs return Reactor's Mono<T> (0..1 value) and Flux<T> (0..N values). Other reactive libraries (RxJava 3, SmallRye Mutiny, Kotlin coroutines) are supported via adapters from org.springframework.core.ReactiveAdapterRegistry (in spring-core). Anywhere Spring accepts a Mono/Flux, it accepts the equivalent in those libraries through the adapter.
End-to-end reactive request
sequenceDiagram
participant Client
participant Server as Reactor Netty
participant DH as DispatcherHandler
participant Filter as WebFilters
participant Ctrl as @RestController
participant DC as DatabaseClient
participant DB as Database
Client->>Server: HTTP request
Server->>DH: ServerWebExchange
DH->>Filter: chain.filter(exchange)
Filter->>DH: passthrough
DH->>Ctrl: handler invocation (reactive arg resolution)
Ctrl->>DC: client.sql(…).bind(…).all()
DC->>DB: SQL via R2DBC driver (non-blocking)
DB-->>DC: rows (streaming)
DC-->>Ctrl: Flux<Row>
Ctrl-->>DH: Mono<ResponseEntity<Flux<User>>>
DH->>Server: subscribe and write
Server-->>Client: HTTP response (streaming, backpressure-aware)No thread blocks at any point. The transport (Reactor Netty) signals demand backwards through the chain; the database produces rows on demand; the framework writes them out as they're available.
Why reactive?
Reactive is not automatically faster. It excels at:
- High-concurrency I/O-bound workloads — chat servers, gateway proxies, fan-out APIs
- Streaming responses — server-sent events, large payloads, slow clients
- Resource-constrained environments — fewer threads needed to handle many in-flight requests
It is not the right choice for:
- Simple CRUD apps with low concurrency
- Apps with heavy CPU-bound work (reactive doesn't help)
- Apps that already saturate available threads in MVC
The framework intentionally keeps both stacks first-class so teams pick by fit, not by recency.
WebFlux annotation model
@RestController
public class UserController {
private final UserRepository repo;
@GetMapping("/users/{id}")
public Mono<User> get(@PathVariable Long id) {
return repo.findById(id);
}
@GetMapping("/users")
public Flux<User> list() {
return repo.findAll();
}
@PostMapping("/users")
public Mono<User> create(@RequestBody Mono<UserCreate> body) {
return body.flatMap(repo::save);
}
}The annotations are the same as in MVC (@GetMapping, @PathVariable, @RequestBody, @ResponseBody). The difference is the return types and that argument resolution can also produce reactive types (Mono<UserCreate> above) — useful for streaming uploads.
Reactive transactions
@Transactional
public Mono<User> register(UserCreate input) {
return userRepo.save(new User(...))
.flatMap(u -> auditRepo.log(...).thenReturn(u));
}The @Transactional annotation is detected at AOP wiring time. Because the method returns Mono, the framework routes through ReactiveTransactionInterceptor instead of the imperative TransactionInterceptor. The transaction state lives in the Reactor Context, not ThreadLocal.
A ReactiveTransactionManager bean (e.g., R2dbcTransactionManager) must be present.
WebClient
WebClient is the reactive HTTP client. It's the recommended client for both reactive and imperative Spring apps that want non-blocking I/O.
WebClient client = WebClient.create("https://api.example.com");
Mono<User> user = client.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class);WebClient supports:
- Streaming bodies (request and response) via
Mono/Flux - Filters (auth, logging, retry)
- Pluggable transport (Reactor Netty, Jetty, HttpClient5)
- Codec configuration via
ExchangeStrategies
Reactive WebSockets and RSocket
spring-webflux includes a reactive WebSocket handler model: WebSocketHandler accepts a WebSocketSession and returns Mono<Void> after composing send/receive flows.
RSocket is a fully bidirectional protocol over TCP/WebSocket. Spring's RSocket support lives in spring-messaging (with WebSocket transport via spring-webflux). @MessageMapping methods double as RSocket handlers and STOMP handlers.
Bridging reactive and imperative
Sometimes you need to call blocking code from a reactive context (e.g., a JDBC repository from a WebFlux controller). The standard approach:
Mono.fromCallable(() -> blockingMethod())
.subscribeOn(Schedulers.boundedElastic())Schedulers.boundedElastic() is a thread pool sized for blocking I/O (default: 10 × CPU cores). Don't use the default Schedulers.parallel() for blocking — it has very limited threads.
The reverse direction (calling reactive code from imperative) is straightforward:
User u = mono.block(); // blocks the calling thread…but block() defeats the purpose in a hot path; prefer to make the entire chain reactive.
BlockHound
The framework's tests use BlockHound to detect accidental blocking calls in reactive flows. It's not enabled by default in user apps but is a useful dependency to add when developing reactive code.
Performance gotchas
- Don't
.block()in handlers — defeats the reactive model - Avoid
ThreadLocal— Reactor flows hop threads; use the ReactorContextinstead - Backpressure matters — A
Fluxthat emits faster than its consumer can handle needs a buffer or sampling strategy - Pin blocking work — Use
subscribeOn(Schedulers.boundedElastic())for blocking I/O
See also
- spring-webflux, spring-r2dbc, spring-messaging, spring-websocket
- web-mvc — imperative counterpart
- data-access — overlapping coverage of
R2dbcTransactionManager
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.