Spring WebFlux: Project Reactor Mono/Flux & Netty EventLoop
- Last updated
- Prerequisites:
- Spring MVC basics
- Understanding of async I/O and event loops
- spring
- webflux
- reactor
- netty
- event-loop
- mono
- flux
- reactive
Read at your depth
The practical view
Spring WebFlux is the reactive stack: controllers return Mono<T> (0-1 values) or Flux<T> (0-N values) instead of blocking values. It runs on Netty (default) with a small number of event-loop threads handling many concurrent requests via non-blocking I/O. The reactive pipeline: return Mono.just(data).map(...).flatMap(service::call).map(...); — nothing blocks; the request completes when the publisher emits. Use WebClient (reactive) instead of RestTemplate. WebFlux shines for I/O-bound apps with many concurrent requests; it does NOT speed up CPU-bound work (still needs parallel schedulers).
The same idea in other frameworks
N/A — server-side WebFlux concern.N/A — WebFlux concern.N/A — WebFlux concern.N/A — WebFlux concern.Legacy vs modern
Blocking MVC controller vs reactive WebFlux controller
MVC blocks a thread per request; WebFlux composes a non-blocking chain that frees the event loop while I/O is in flight.
@GetMapping("/order/{id}")
public Order get(@PathVariable long id) {
User u = userClient.get(id); // ❌ OLD: blocks a thread
return orderService.findByUser(u); // ❌ OLD: blocks again
}@GetMapping("/order/{id}")
public Mono<Order> get(@PathVariable long id) {
return userClient.get(id) // ✅ NEW: non-blocking
.flatMap(orderService::findByUser); // ✅ NEW: composed, non-blocking
}Interview gotchas
Context
This is the critical WebFlux concept — interviewers want the event-loop starvation mechanism and why the same blocking call that's merely 'wasteful' in MVC is fatal in the reactive model.
The mechanical answer
In MVC, a blocking call parks one thread; with 200 threads, 200 concurrent blocking calls is the capacity limit but not a deadlock. In WebFlux, the event loop threads are SHARED across all connections: if a handler blocks (Thread.sleep, a blocking JDBC call, a blocking RestTemplate), it stalls that event-loop thread, and because that same thread serves hundreds of other connections, ALL of them are delayed — effectively freezing a large slice of the server. Netty has few event loops (typically 2×cores), so a handful of blocking handlers can starve the entire server. This is why the rule is: end-to-end non-blocking (reactive drivers, WebClient, reactive repositories), and if you must block (e.g. a legacy library), offload it with subscribeOn/publishOn(Schedulers.boundedElastic()) so the event loop is never blocked.
Trap
The naive answer is 'blocking just costs one thread, same as MVC'. It stalls a shared event loop and every connection multiplexed on it. Another trap: assuming WebFlux makes CPU-bound work faster — it doesn't; CPU work still occupies the event loop and should run on the parallel scheduler. Candidates who describe event-loop starvation show real reactive understanding.
Context
This probes the reactive pipeline mechanics — interviewers want the subscriber-driven pull model and why nothing executes until someone subscribes, which explains many WebFlux debugging surprises.
The mechanical answer
Mono and Flux are lazy publishers: constructing them and chaining operators only BUILDS a description of the pipeline — nothing runs until a subscriber subscribes. On subscribe, the subscription flows DOWNWARD (from subscriber toward the source) as each operator requests data via the request(n) backpressure signal; then data flows UPWARD: the source emits, each operator processes and passes the result to the next subscriber. This pull-based model is what enables backpressure — consumers can request one element at a time, and producers respect the demand. In WebFlux, the framework subscribes to the returned Mono/Flux, drives it to completion, and writes the response. This laziness means side effects inside operators run at subscription time (possibly multiple times if re-subscribed), which is why operators should be pure and shared publishers should be cached.
Trap
The naive answer is 'the code runs when the controller returns'. It runs only when the framework subscribes, and each subscription re-runs the chain — a Mono built inside a method that's called twice re-executes its source twice. Another trap: blocking inside a map() to 'wait for the value' — that's the event-loop starvation bug again; you should use flatMap to return a Mono. Candidates who explain the subscribe-drives-execution model show real Reactor depth.