Reactive Spring (WebFlux): what it is, and when not to use it
You will hear about reactive Spring — WebFlux, Mono, Flux, "non-blocking" — and it is easy to feel
you should be using it. This lesson is an honest overview: what reactive Spring is, the model it uses, and —
most importantly — when it is worth the considerable cost and when it is not. The conclusion up front,
because it saves people a lot of grief: most applications should use the standard, blocking Spring MVC you
have learnt this whole course; reactive is a specialised tool for a specific problem.
Two Spring web stacks
Spring has two ways to build web applications:
- Spring MVC (servlet stack) — the traditional, blocking, thread-per-request model this course uses. Each request is handled by a thread that blocks while waiting for the database or an external call. Simple, familiar, and what the vast majority of Spring apps use.
- Spring WebFlux (reactive stack) — a non-blocking, event-loop model built on Project Reactor. A small number of threads handle many requests by never blocking: instead of a thread waiting on I/O, the work is registered as a callback and the thread moves on, resuming when the I/O completes.
They solve the request the same way conceptually (controllers, DI, the same Spring core) but with fundamentally different execution models. WebFlux is not "MVC but faster" — it is a different programming model with different types and different rules.
The reactive model: Mono, Flux, and non-blocking
In WebFlux you do not return a Parcel or a List<Parcel>; you return a publisher that will emit them
asynchronously:
Mono<T>— a publisher of zero or one item (one parcel, or none).Flux<T>— a publisher of zero to many items (a stream of parcels).
@GetMapping("/{id}")
public Mono<Parcel> getOne(@PathVariable Long id) {
return parcelRepository.findById(id); // returns a Mono, not a Parcel — nothing blocks
}
You compose operations on these with operators (map, flatMap, filter) rather than imperative code, and
crucially nothing may block — every layer, including the database driver (R2DBC, not JDBC/JPA), must be
non-blocking, or you defeat the entire model by parking one of the few event-loop threads. This is the big
catch: going reactive is all-in. You cannot use blocking JPA/JDBC in a WebFlux app without undermining it;
the whole stack — web layer, data access, external calls — must be reactive.
What reactive is actually for
Reactive's genuine benefit is handling very high concurrency with few threads, specifically when the work is I/O-bound — lots of requests each mostly waiting (on other services, slow upstreams, streaming connections). Because a non-blocking thread does not sit idle during I/O, a handful of threads can serve a huge number of concurrent, mostly-waiting requests, using less memory than thread-per-request would. The cases where it earns its complexity:
- Massive concurrency of I/O-bound requests — an API gateway or aggregator fanning out to many slow services, tens of thousands of concurrent connections.
- Streaming — server-sent events, long-lived connections, backpressure-sensitive data streams, where the reactive model's flow control is a real advantage.
- A fully reactive stack already — if your datastore and dependencies are non-blocking, reactive is natural.
Notice how specific this is. It is the async-views story from the Django course, at the framework level: reactive helps when you are drowning in concurrent waits, not when you are doing ordinary CRUD.
Why most apps should NOT use it
The honest, important part. Reactive carries a large cost that most applications should not pay:
- The programming model is much harder. Everything is
Mono/Fluxand operator chains; debugging is harder (stack traces are not linear), and the mental model is a significant step up from imperative code. - The whole stack must be non-blocking. No JPA/JDBC (you need R2DBC, which is less mature and less capable than JPA), no blocking libraries — a major constraint that rules out much of the ecosystem you just learned.
- It does not make CPU-bound work faster. Like async in general, reactive helps you wait efficiently, not compute faster. If your bottleneck is the database doing work (not you waiting on it), reactive gives nothing.
- Most apps are not I/O-concurrency-bound. A typical CRUD backend serving thousands (not hundreds of thousands) of requests, each doing a few quick queries, is served perfectly by blocking MVC — the threads are not the bottleneck. Adopting reactive here adds enormous complexity for no benefit.
The strong, mainstream guidance: use Spring MVC (blocking) unless you have a specific, measured need for massive I/O-bound concurrency or streaming that the thread-per-request model cannot meet. Dakiya — a normal CRUD courier API — is exactly a Spring MVC app. Reaching for WebFlux because it sounds modern is a classic over-engineering mistake that teams regret.
The judgement to carry
Reactive Spring is a powerful, specialised tool, and knowing of it — what Mono/Flux are, that the
whole stack must be non-blocking, what problem it solves — is part of being a competent Spring developer.
But the more valuable judgement is knowing when not to use it. If someone proposes rewriting a CRUD service
in WebFlux, the right questions are: are we actually I/O-concurrency-bound? have we measured that
thread-per-request is the bottleneck? is our whole stack able to go non-blocking? Usually the answer is no,
and the honest recommendation is to stay with the blocking MVC stack you know. Reactive earns its place at
the specific edge — extreme concurrent I/O, streaming — and nowhere else.
Check your work
Two stacks. Spring MVC (servlet, blocking, thread-per-request — this course, and most apps) vs WebFlux (reactive, non-blocking, event-loop — Project Reactor). Not "MVC faster" — a different model.
The reactive model. Return Mono<T> (0-1) or Flux<T> (0-many) publishers, composed with operators;
nothing may block — the whole stack (including the DB driver: R2DBC, not JPA) must be non-blocking, or
you defeat it.
What it is for. Very high concurrency of I/O-bound requests (gateways/aggregators, tens of thousands of connections) and streaming — waiting efficiently with few threads.
Why most apps should not. Much harder model and debugging; the whole stack must be non-blocking (no JPA/JDBC); no help for CPU-bound work; most apps are not I/O-concurrency-bound. Blocking MVC serves typical CRUD perfectly.
The judgement. Use MVC unless you have a specific, measured need for massive I/O concurrency or streaming. Adopting reactive for ordinary CRUD is over-engineering.
Practice
- Explain
Mono<T>vsFlux<T>and how a WebFlux controller differs from an MVC one (returns a publisher, nothing blocks). - Articulate why a WebFlux app cannot use JPA/JDBC without undermining itself, and what it uses instead (R2DBC).
- For three systems (a CRUD courier API, an API gateway fanning out to 20 slow services, a CPU-heavy report generator), decide MVC vs WebFlux and justify each.
- Explain why reactive does not speed up CPU-bound work (it helps you wait, not compute).
- Argue, for Dakiya, why Spring MVC is the right stack — naming what would have to be true to justify WebFlux instead.
- Describe the questions you would ask before agreeing to rewrite a service in WebFlux.
Official documentation
- Spring — Web on Reactive Stack (WebFlux) — The reactive stack.
- Spring — Choosing MVC vs WebFlux — Spring's own guidance on when to use which.
- Project Reactor — Mono and Flux — The reactive types and operators.
Next: what to test and what to skip.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship