What's the Difference Between Spring Web (MVC) and Spring WebFlux?
Problem
I’m starting a new Spring Boot project, and I keep seeing references to both Spring Web (MVC) and Spring WebFlux. I’m not sure which one to choose.
Some sources say WebFlux is “better” because it’s non-blocking. Others say to stick with Spring Web. What’s the actual difference?
Environment
- Spring Boot 3.x
- Java 17+ (or Java 21+ with virtual threads)
What happened?
I tried WebFlux because I read it handles more requests with fewer threads. But I ran into problems.
First, my existing code using ThreadLocal broke:
// This worked fine in Spring MVCpublic class UserContext { private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setUser(User user) { currentUser.set(user); }
public static User getUser() { return currentUser.get(); }}In WebFlux, this caused a production incident. Users started seeing each other’s data because the same thread switches between different requests.
Then, my JDBC calls blocked the event loop:
@GetMapping("/users/{id}")public Mono<User> getUser(@PathVariable Long id) { // This blocks the event loop! User user = userRepository.findById(id); // JDBC is blocking return Mono.just(user);}The entire application became slow because blocking calls tie up the few event loop threads.
How to solve it?
I learned that I should match the framework to my actual needs:
Stay with Spring Web (MVC) if:
- Building CRUD APIs and web applications
- Using JDBC/JPA (blocking databases)
- Using
ThreadLocalfor user context, logging, or transactions - Your team doesn’t have reactive experience
- Using Java 21+ with virtual threads
Use Spring WebFlux if:
- Building streaming APIs (SSE, WebSocket)
- Your entire stack is reactive (R2DBC, reactive Redis)
- You need backpressure control
- High concurrency on limited hardware
Spring Web MVC Controller
@RestController@RequestMapping("/users")public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) { this.userRepository = userRepository; }
@GetMapping("/{id}") public User getUser(@PathVariable Long id) { // Simple, linear code return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); }}Spring WebFlux Controller
@RestController@RequestMapping("/users")public class UserController {
private final ReactiveUserRepository userRepository;
public UserController(ReactiveUserRepository userRepository) { this.userRepository = userRepository; }
@GetMapping("/{id}") public Mono<User> getUser(@PathVariable Long id) { // Reactive chain - nothing happens until subscribe return userRepository.findById(id) .switchIfEmpty(Mono.error(new UserNotFoundException(id))); }}You can see the key difference: MVC returns objects directly, WebFlux returns Mono or Flux wrappers.
The reason
The core difference is the threading model:
┌─────────────────────────────────────────────────────────────────┐│ Spring Web (MVC) │├─────────────────────────────────────────────────────────────────┤│ Request ──▶ Thread 1 ──▶ Controller ──▶ Service ──▶ Response ││ Request ──▶ Thread 2 ──▶ Controller ──▶ Service ──▶ Response ││ Request ──▶ Thread 3 ──▶ Controller ──▶ Service ──▶ Response ││ ││ Thread-per-request model. Blocking is okay. │└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐│ Spring WebFlux │├─────────────────────────────────────────────────────────────────┤│ Request ──┐ ││ Request ──┼──▶ Event Loop Thread ──▶ Non-blocking I/O ──▶ ... ││ Request ──┘ ││ ││ Few threads handle many requests. Blocking breaks everything. │└─────────────────────────────────────────────────────────────────┘Why ThreadLocal breaks in WebFlux
In Spring MVC, each request has its own thread:
Request A arrives ──▶ Thread 1 handles itRequest B arrives ──▶ Thread 2 handles itThreadLocal values stay isolatedIn WebFlux, threads switch between requests:
Request A arrives ──▶ Event loop thread starts processing ┌── Request A pauses (waiting for DB)Request B arrives ────┘── Same thread now handles Request B └── ThreadLocal from Request A leaks to Request B!This is why the production incident happened. Users saw each other’s data.
The virtual threads game-changer
With Java 21, I can enable virtual threads in Spring MVC:
spring.threads.virtual.enabled=trueNow each Tomcat request runs on a virtual thread. Blocking I/O doesn’t tie up platform threads. I get the simplicity of Spring MVC with near-reactive concurrency.
Comparison table
| Aspect | Spring Web (MVC) | Spring WebFlux |
|---|---|---|
| Server | Tomcat, Jetty | Netty |
| Threading | Thread-per-request | Event loop |
| I/O Model | Blocking | Non-blocking |
| Return Types | Direct objects | Mono<T>, Flux<T> |
| ThreadLocal | Works normally | Dangerous |
| Debugging | Straightforward | Complex async chains |
| Learning Curve | Familiar | Steep |
Common mistakes
Mistake 1: Using WebFlux without understanding reactive programming
Once they tried to use ThreadLocal on WebFlux as a resultusers from different cities and countries started seeingeach other's data in production.Mistake 2: Mixing blocking code in WebFlux
@GetMapping("/users/{id}")public Mono<User> getUser(@PathVariable Long id) { // This blocks the event loop! User user = jdbcTemplate.queryForObject(...); return Mono.just(user);}Mistake 3: Choosing WebFlux for the wrong reasons
Most applications don’t need WebFlux performance. The complexity cost outweighs the benefits. Use Spring MVC unless you have a specific reason.
Mistake 4: Not updating the entire stack
If you use WebFlux, everything must be reactive:
- JDBC → R2DBC
- JPA → No direct equivalent (use R2DBC entities)
- Blocking HTTP clients → WebClient
Summary
In this post, I explained the difference between Spring Web and Spring WebFlux. The key point is that Spring Web is the right choice for most applications. WebFlux has specific use cases: streaming APIs, fully reactive stacks, and high-concurrency scenarios on limited hardware. If you choose WebFlux, understand that ThreadLocal doesn’t work the same way, debugging is harder, and your entire stack must be non-blocking.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Spring Framework Documentation: Web MVC
- 👨💻 Spring Framework Documentation: WebFlux
- 👨💻 Spring Blog: Embracing Virtual Threads
- 👨💻 Project Reactor Reference
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments