Spring WebFlux and Spring MVC are both powerful frameworks for building web applications in Spring, but they approach concurrency and I/O very differently, leading to distinct performance characteristics and use cases.
Let’s see what happens when we hit a slow external service with both. Imagine we have two simple Spring Boot applications: one using WebFlux and one using MVC. Each has a single endpoint that calls a simulated slow external API.
Spring MVC (Blocking I/O)
// MVC Controller
@RestController
public class MvcController {
@GetMapping("/mvc/slow")
public String slowRequest() throws InterruptedException {
System.out.println("MVC Request received. Thread: " + Thread.currentThread().getName());
Thread.sleep(5000); // Simulate 5-second blocking I/O
System.out.println("MVC Request finished. Thread: " + Thread.currentThread().getName());
return "MVC Slow Request Done";
}
}
When a request comes into the MVC app, it’s handled by a thread from a fixed-size thread pool (typically Tomcat’s tomcat-exec-* threads). If Thread.sleep(5000) is called, that entire thread is blocked for 5 seconds. During that time, it cannot process any other incoming requests. If you send 10 concurrent requests to this endpoint, and your thread pool has only 10 threads, the 11th request will have to wait until one of the first 10 threads is freed up.
Spring WebFlux (Non-Blocking I/O)
// WebFlux Controller
@RestController
public class WebfluxController {
@GetMapping("/webflux/slow")
public Mono<String> slowRequest() {
System.out.println("WebFlux Request received. Thread: " + Thread.currentThread().getName());
return Mono.delay(Duration.ofSeconds(5)) // Simulate 5-second non-blocking I/O
.doOnSuccess(v -> System.out.println("WebFlux Request finished. Thread: " + Thread.currentThread().getName()))
.thenReturn("WebFlux Slow Request Done");
}
}
In WebFlux, requests are typically handled by a small number of event loop threads (often Netty’s nioEventLoopGroup-* threads). When Mono.delay(Duration.ofSeconds(5)) is called, the request handling thread is not blocked. Instead, it registers a callback with the underlying I/O system and returns immediately. The event loop thread is then free to pick up other incoming requests. Once the 5 seconds have passed and the Mono.delay completes, the event loop thread will be notified and can resume processing the response for that specific request. With only a few event loop threads, WebFlux can handle thousands of concurrent requests that involve waiting for I/O, because the threads are never idle waiting for I/O to complete.
The core problem WebFlux solves is the inefficient use of threads under high I/O load. In traditional, blocking MVC applications, each concurrent request that performs I/O (like a database query or an HTTP call) typically occupies a dedicated thread for the entire duration of that I/O operation. If you have many such requests happening concurrently, you quickly exhaust your thread pool, leading to request queuing, increased latency, and eventually OutOfMemoryError if thread stacks grow too large. WebFlux, by embracing reactive programming and non-blocking I/O, allows a small number of threads to manage a vast number of concurrent operations by only using threads when actively processing data, not when waiting.
Here’s a breakdown of how it works internally:
- MVC (Servlet API): Built on the Servlet API. Each incoming request is mapped to a thread from a thread pool managed by the servlet container (e.g., Tomcat, Jetty). This thread executes the request handler. If the handler performs a blocking I/O operation (like
RestTemplate.getForObject()), the thread is suspended and cannot do anything else until the I/O completes. The thread pool size is a critical tuning parameter. - WebFlux (Reactive Streams API): Built on reactive libraries like Reactor (for
MonoandFlux) and Netty or Undertow as the non-blocking server. It uses an event-loop model. A small, fixed number of threads (often 1-2 per CPU core) are responsible for handling many concurrent connections. When an I/O operation is initiated, the thread registers a callback and returns. The event loop thread is then free to process other events or requests. When the I/O completes, a callback is triggered, and the event loop thread picks up where it left off for that specific request.
To manage these different models, you control the core components:
- MVC:
- Servlet Container: Tomcat, Jetty, Undertow.
- Concurrency Model: Thread-per-request.
- I/O: Blocking by default. You can use
WebClientfor non-blocking calls, but the controller itself remains blocking. - Key Configuration:
server.tomcat.threads.max(e.g.,server.tomcat.threads.max=200).
- WebFlux:
- Reactive Server: Netty, Undertow, or even the Servlet API (if running on a compatible Servlet 3.1+ container and configured correctly).
- Concurrency Model: Event-loop, non-blocking I/O.
- I/O: Non-blocking by design. Uses
WebClientfor outgoing calls. - Key Configuration:
spring.webflux.netty.worker-threads(if using Netty, defaults to2 * number_of_cores).
When you use Mono or Flux in a WebFlux controller, you’re not just returning a value; you’re returning a description of how to produce that value asynchronously. The Mono<String> returned by the slowRequest method is a publisher that will eventually emit a String. The WebFlux runtime subscribes to this publisher and handles the execution flow, including the asynchronous delay.
The one thing most people don’t realize is that WebFlux doesn’t magically make your entire application non-blocking. If your WebFlux controller calls a traditional, blocking JDBC driver or a library that doesn’t support reactive I/O, you’ll still need to offload that blocking operation to a separate, dedicated thread pool (like Spring’s TaskExecutor) to avoid blocking the event loop threads. This is often done using Mono.fromCallable() or Flux.fromIterable() with a publishOn(Schedulers.boundedElastic()) operator to ensure the blocking call happens on a different thread pool.
Choosing between MVC and WebFlux depends heavily on your application’s I/O patterns. For traditional CRUD applications with moderate concurrency and primarily synchronous I/O, MVC is often simpler and more straightforward. For high-concurrency scenarios, microservices that are heavily I/O-bound (e.g., API gateways, services making many downstream calls), or applications embracing a fully reactive stack, WebFlux offers significant scalability benefits.
The next hurdle you’ll likely face is understanding how to manage state and side effects in a reactive, non-blocking world, especially when dealing with concurrent modifications or complex transaction management.