Reactive Programming
Reactive programming is an asynchronous, non-blocking style of writing code that handles streams of data with backpressure. In Spring Boot it is delivered through Spring WebFlux and Project Reactor, an alternative to the traditional blocking Spring MVC stack. This section explains the model and shows when it earns its keep — and when it just adds complexity.
What “non-blocking” actually means
In the classic thread-per-request model used by Spring MVC, every incoming HTTP request occupies one thread for its entire lifetime. When that thread calls a database or another service, it blocks — it sits idle waiting for I/O while still consuming memory (a stack, typically ~1 MB). Under high concurrency you run out of threads long before you run out of CPU.
The reactive model flips this. A small pool of event loop threads (often one per CPU core) never block. When a handler issues an I/O call, it registers a callback and immediately moves on to other work. The result is delivered later, when the data is ready. A handful of threads can juggle tens of thousands of concurrent connections.
Thread-per-request (MVC): Event loop (WebFlux):
req → [Thread 1] ───wait── req ─┐
req → [Thread 2] ───wait── req ─┼→ [event loop] ⇄ non-blocking I/O
req → [Thread 3] ───wait── req ─┘ (handful of threads)
(1 thread blocked per call) (no thread ever waits)
The Reactive Streams specification
Reactive Streams is a tiny standard (now part of the JDK as java.util.concurrent.Flow) that defines how asynchronous stream processing with non-blocking backpressure should work. It is built on four interfaces.
| Interface | Role |
|---|---|
Publisher<T> | Produces a (possibly infinite) sequence of items |
Subscriber<T> | Consumes items, errors, and a completion signal |
Subscription | The link between them; the subscriber uses it to request items |
Processor<T,R> | Both a subscriber and a publisher (a stage in a pipeline) |
The crucial idea is backpressure. A subscriber calls subscription.request(n) to ask for only as many items as it can handle. A fast publisher can never overwhelm a slow consumer, because the consumer controls the flow. This is the safety valve that makes streaming over the network reliable.
Subscriber ── request(10) ──▶ Publisher
Subscriber ◀── onNext × 10 ── Publisher
Subscriber ── request(10) ──▶ Publisher (asks for more when ready)
Subscriber ◀──── onComplete ── Publisher
Note: Project Reactor’s
MonoandFluxare concretePublisherimplementations. You almost never implement the raw interfaces yourself — see Mono & Flux.
When reactive helps
Reactive shines when threads would otherwise spend their lives waiting:
- High concurrency, I/O-bound workloads — API gateways, BFF layers, and services that mostly fan out to other services or databases.
- Streaming — Server-Sent Events, large file transfers, or live feeds where you push data as it arrives rather than buffering it all in memory.
- Many slow clients — mobile or IoT devices on poor networks where connections stay open a long time.
In these scenarios a reactive service handles far more concurrent requests on the same hardware, with smaller, more stable memory usage.
When reactive hurts
Reactive is not a free lunch. Be honest about the costs:
- Cognitive overhead — the whole pipeline becomes a chain of operators. Stack traces are harder to read and step-debugging is awkward.
- Blocking drivers poison the pool — one accidental blocking call (JDBC,
RestTemplate, file I/O) on an event loop thread can stall thousands of requests. JPA/Hibernate is fundamentally blocking and cannot be used reactively; you need R2DBC instead. - Smaller ecosystem — fewer libraries have reactive equivalents, and team familiarity is usually lower.
- CPU-bound work gains nothing — if your bottleneck is computation, not waiting, the event loop just gets in the way.
Tip: Since Java 21, virtual threads give much of the scalability of non-blocking I/O while keeping the simple, readable blocking programming model. For many applications they are the better trade-off. See the honest comparison in WebFlux vs Spring MVC.
A first taste
A reactive endpoint returns a Publisher (Mono or Flux) instead of a value. Spring subscribes to it and writes the result to the response as it arrives — no thread is parked while the data is fetched.
@RestController
class GreetingController {
@GetMapping("/hello/{name}")
Mono<String> hello(@PathVariable String name) {
return Mono.just("Hello, " + name + "!"); // 0..1 result, no blocking
}
}
Compare this with a blocking MVC handler that returns String directly. The shape is similar, but the reactive version composes asynchronously and never holds a thread hostage.
In This Section
- Mono & Flux — Project Reactor’s core publisher types and operators.
- Spring WebFlux — the reactive web framework, annotated and functional styles.
- Reactive REST APIs — non-blocking CRUD, streaming, and
WebClient. - R2DBC (Reactive SQL) — non-blocking access to relational databases.
- WebFlux vs Spring MVC — when to choose which (and virtual threads).
Related Topics
- Building REST APIs — the blocking Spring MVC counterpart.
- Mono & Flux — the building blocks of every reactive pipeline.
- WebFlux vs Spring MVC — an honest decision guide.
- Java Streams — lazy, declarative pipelines (the synchronous analogy).