Skip to content

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:

UserContext.java
// This worked fine in Spring MVC
public 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:

UserController.java
@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 ThreadLocal for 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

UserController.java
@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

UserController.java
@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:

Spring MVC Threading
Request A arrives ──▶ Thread 1 handles it
Request B arrives ──▶ Thread 2 handles it
ThreadLocal values stay isolated

In WebFlux, threads switch between requests:

WebFlux Threading
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:

application.properties
spring.threads.virtual.enabled=true

Now 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

AspectSpring Web (MVC)Spring WebFlux
ServerTomcat, JettyNetty
ThreadingThread-per-requestEvent loop
I/O ModelBlockingNon-blocking
Return TypesDirect objectsMono<T>, Flux<T>
ThreadLocalWorks normallyDangerous
DebuggingStraightforwardComplex async chains
Learning CurveFamiliarSteep

Common mistakes

Mistake 1: Using WebFlux without understanding reactive programming

Production Incident Report
Once they tried to use ThreadLocal on WebFlux as a result
users from different cities and countries started seeing
each other's data in production.

Mistake 2: Mixing blocking code in WebFlux

Bad: Blocking 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments