Spring MVC assigns a thread to each incoming request. That thread waits for DB query results, waits for external API responses. In I/O-bound workloads, most threads end up waiting. WebFlux changes this structure with an event loop-based non-blocking model.
Spring MVC’s Thread Model
Spring MVC uses a thread-per-request model. When Tomcat receives a request, it pulls a thread from the pool and assigns it. That thread handles the entire lifecycle: controller → service → database access → response.
The problem is I/O wait time. Executing a JDBC query blocks the thread until results arrive. Calling an external API with RestTemplate does the same. The thread consumes resources while doing nothing.
Concurrent request capacity is bound by thread pool size. Tomcat defaults to 200 threads. The 201st request queues until an earlier one completes. Throughput drops sharply when I/O waits grow long.
WebFlux’s Event Loop Model
WebFlux runs on a Netty-based event loop. When a thread receives a request, it delegates I/O operations to the OS and immediately moves to the next request. When I/O completes, a callback delivers the result and the remaining logic executes.
Threads never wait, so a small number of threads handle many concurrent requests. Event loop threads equal to the CPU core count can manage thousands of simultaneous connections.
Throughput increases for I/O-bound workloads. API gateways, OAuth2 authentication servers, and inter-service communication in microservice architectures are typical examples.
CPU-bound tasks see no benefit. Running heavy computation on an event loop thread blocks other request processing. Image processing, encryption, and similar work require offloading to a separate thread pool.
Reactor
The event loop model relies on callbacks. Nested callbacks increase code complexity. Project Reactor solves this with declarative pipelines. It provides two core types.
Mono: Returns 0 or 1 result asynchronously. Used for looking up a single user from the database or receiving a token from an external API.
Flux: Returns 0 to N results as an asynchronous stream. Used for querying lists from the database or subscribing to real-time events.
@GetMapping("/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userRepository.findById(id);
}
@GetMapping
public Flux<User> getAllUsers() {
return userRepository.findAll();
}
Reactor composes asynchronous pipelines through operator chaining.
public Mono<AuthToken> login(String code) {
return oauth2Client.getToken(code) // Mono<TokenDto>
.map(tokenMapper::toDomain) // Mono<Token>
.flatMap(userRepository::upsertUser) // Mono<User>
.map(authService::generateToken); // Mono<AuthToken>
}
map performs synchronous transformation; flatMap performs asynchronous transformation. Use flatMap for operations that return another Mono or Flux, such as I/O calls.
One important characteristic: Reactor executes at subscription time. Creating a Mono or Flux does nothing on its own. The pipeline runs only when .subscribe() is called or when WebFlux returns it as a response.
WebClient
RestTemplate is blocking. Running a blocking call on an event loop thread occupies that thread and degrades overall throughput. In WebFlux environments, use WebClient — the non-blocking HTTP client.
public Mono<UserDto> getResource(String accessToken) {
return webClient
.get()
.uri(uri -> uri.queryParam("access_token", accessToken).build())
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, this::handleError)
.bodyToMono(UserDto.class);
}
retrieve() receives the response; bodyToMono() deserializes it. The entire process is non-blocking. onStatus() defines error handling declaratively by HTTP status.
WebClient works in Spring MVC too. RestTemplate entered maintenance mode in Spring 5, and Spring recommends WebClient as its replacement.
Reactive Data Access
The reactive stack delivers its full benefit when the entire pipeline is non-blocking. If the controller and service are non-blocking but database access blocks, the event loop thread stalls.
MongoDB: Spring Data provides ReactiveMongoRepository. All CRUD methods return Mono or Flux.
interface UserDao extends ReactiveMongoRepository<UserEntity, String> {}
Relational databases: Use R2DBC (Reactive Relational Database Connectivity), the reactive alternative to JDBC. Drivers for MySQL, PostgreSQL, and others are available.
JPA is blocking. Hibernate and JDBC block threads, so using JPA with WebFlux eliminates non-blocking benefits. Choose R2DBC for relational databases or Reactive MongoDB for document stores.
When to Choose Which
WebFlux and Spring MVC are not mutually exclusive. Spring designed them to coexist in the same project. In practice, unifying the entire stack under one model works better.
WebFlux fits when:
- I/O-bound workloads dominate
- High concurrency is required (API gateways, authentication servers)
- Inter-service communication is frequent
- The data store has reactive drivers (MongoDB, Redis)
Spring MVC fits when:
- JDBC/JPA-based relational databases are central
- CPU-bound tasks dominate
- The team is unfamiliar with reactive programming
Spring MVC assigns one thread per request. When I/O grows, threads sit idle. WebFlux removes that wait time with an event loop. Which model fits comes down to whether the workload is I/O-bound or CPU-bound, and how much concurrency it needs.