WebFlux vs Spring MVC
Should you build your next service on Spring WebFlux or stick with Spring MVC? This page gives an honest, practical comparison across the dimensions that actually matter — and explains why, since Java 21, virtual threads make MVC the right default for many of the workloads people used to reach for WebFlux to solve.
The two stacks at a glance
| Spring MVC | Spring WebFlux | |
|---|---|---|
| Model | thread-per-request, blocking | event loop, non-blocking |
| Server | Tomcat (default) | Netty (default) |
| Return types | T, List<T>, ResponseEntity | Mono<T>, Flux<T> |
| Data access | JDBC, JPA | R2DBC, reactive Mongo |
| HTTP client | RestClient, RestTemplate | WebClient |
| Concurrency model | one thread per request | a few event loop threads |
Programming model
MVC is straightforward, imperative code: call a method, get a value, handle the exception with try/catch. Stack traces read top to bottom and every developer already understands it.
WebFlux is declarative pipelines of operators. It is powerful and composes beautifully for streaming and fan-out, but the learning curve is real: flatMap vs map, hot vs cold publishers, schedulers, and the rule that one stray blocking call breaks everything.
Throughput and resource usage
Under high concurrency with I/O-bound work, WebFlux handles more simultaneous connections per unit of memory because it does not park a thread per request. This is its headline benefit and it is genuine — an API gateway fanning out to many slow services is a textbook fit.
But the picture is nuanced:
- For CPU-bound work, both stacks are limited by cores; reactive adds overhead and wins nothing.
- For moderate concurrency, a well-tuned MVC app on Tomcat is often plenty and far simpler.
- WebFlux’s advantage evaporates the moment any blocking dependency appears in the chain.
Debugging and observability
| Concern | Spring MVC | Spring WebFlux |
|---|---|---|
| Stack traces | clear, full call stack | fragmented across operators |
| Step debugging | natural | awkward (async boundaries) |
ThreadLocal (MDC, security, tx) | works normally | needs Reactor Context plumbing |
| Profilers/tooling | mature | improving but trickier |
Reactor offers help (Hooks.onOperatorDebug(), checkpoint(), reactor-tools), but debugging a reactive flow is meaningfully harder than a blocking one.
Ecosystem maturity
MVC has decades of libraries, examples, and Stack Overflow answers. The reactive ecosystem is solid but smaller — and crucially, many popular libraries are blocking-only. JPA/Hibernate, most JDBC-backed tools, many SDKs, and a lot of legacy integrations have no reactive equivalent. If a key dependency blocks, WebFlux is the wrong choice.
The blocking-driver problem
This deserves its own warning because it sinks more WebFlux projects than any other issue.
Warning: A single blocking call on a Netty event loop thread (JDBC,
RestTemplate, blocking file/network I/O) stalls every request that thread is serving. You cannot use Spring Data JPA reactively — you must switch to R2DBC and accept its reduced ORM feature set. Going reactive is an all-or-nothing commitment for your whole I/O chain.
Virtual threads: the simpler alternative
Java 21 introduced virtual threads (Project Loom). These are lightweight threads scheduled by the JVM, so you can have millions of them. The runtime automatically unmounts a virtual thread from its carrier when it blocks on I/O and remounts it when the data is ready.
The payoff: you keep the simple, blocking, imperative MVC programming model — ordinary JDBC, JPA, RestClient, readable stack traces, working ThreadLocals — while getting much of the scalability that previously required WebFlux. Enable it with one property:
spring:
threads:
virtual:
enabled: true
For a great many I/O-bound services, this is the sweet spot: MVC’s simplicity plus high concurrency, without rewriting everything into reactive pipelines.
Note: Virtual threads do not replace WebFlux for streaming with backpressure (e.g.
text/event-streamFlux). When you need fine-grained flow control over a stream, Reactor still wins.
A decision guide
| Choose… | When… |
|---|---|
| Spring MVC + virtual threads | most I/O-bound apps; you want simplicity and high concurrency; your drivers (JPA/JDBC) are blocking |
| Spring MVC (platform threads) | CPU-bound work; modest concurrency; legacy/Java 17 codebases |
| Spring WebFlux | streaming with backpressure; extreme connection counts; a fully reactive stack (R2DBC + WebClient); you already have reactive expertise |
Recommendation
Default to Spring MVC, and turn on virtual threads if you need to scale I/O-bound concurrency. Reach for WebFlux deliberately, when you have a streaming/backpressure requirement or a genuinely all-reactive architecture — and when your team is ready for the added complexity. Do not adopt reactive “for performance” by reflex; with virtual threads the simpler stack now covers most cases.
Related Topics
- Reactive Programming — the non-blocking model explained.
- Spring WebFlux — the reactive web framework.
- R2DBC (Reactive SQL) — reactive database access.
- Building REST APIs — the blocking MVC stack.
- Spring Data JPA — blocking persistence (use with virtual threads).
- Async —
@Asyncand background work in the MVC model.