Why More Teams Are Choosing Spring WebFlux for High‑Concurrency Applications

The article explains how Spring WebFlux replaces the thread‑per‑request model with an event‑loop, non‑blocking I/O and reactive streams, offering higher resource efficiency, built‑in back‑pressure, and better support for real‑time data, while also discussing its drawbacks and when virtual threads may be a viable alternative.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Why More Teams Are Choosing Spring WebFlux for High‑Concurrency Applications

Introduction

Spring WebFlux, introduced in Spring 5.0, has sparked a debate: some call it the future of Java web development, others see it as unnecessary complexity. The core problem it addresses is the inefficiency of the traditional "one request, one thread" model in I/O‑intensive scenarios.

Problem with the Blocking Model

In a typical Spring MVC controller, each incoming request occupies a servlet thread for the entire duration of the call, even while waiting for I/O such as a remote service. If 1,000 concurrent requests arrive, the container must allocate at least 1,000 threads, each consuming ~1 MB of stack memory. This leads to excessive context switching and can exhaust resources.

Core issue: threads sit idle while waiting for I/O, yet they hold valuable resources.

WebFlux’s Solution

WebFlux adopts a reactive, event‑driven model that uses a small, fixed number of Netty event‑loop threads to handle many connections. I/O operations are non‑blocking; when data is ready, a callback notifies the system. This allows tens of thousands of concurrent connections with far fewer threads.

Key benefit: fewer resources support higher concurrency.

Thread‑Model Comparison

Spring MVC: Thread‑per‑Request, blocking I/O, concurrency limited by thread‑pool size (typically 200‑500).

Spring WebFlux: Event‑Loop + few threads, non‑blocking I/O, theoretical concurrency of tens to hundreds of thousands.

In a benchmark processing 10,000 long‑lived connections, WebFlux’s CPU usage was about 40 % lower and memory consumption 25 % lower than a synchronous servlet stack.

Reactive Types: Mono and Flux

WebFlux is built on Project Reactor. Mono represents an asynchronous sequence of 0‑1 items, while Flux represents 0‑N items. Methods return these types immediately without waiting for data.

@GetMapping("/user/{id}")
public Mono<User> getUser(@PathVariable Long id) {
    return userRepository.findById(id); // async, returns Mono
}

@GetMapping("/users")
public Flux<User> getUsers() {
    return userRepository.findAll(); // async, returns Flux
}

Building a Minimal WebFlux Application

1. Add the spring-boot-starter-webflux dependency (do not include spring-boot-starter-web – they are mutually exclusive).

2. Define a reactive controller that returns Mono or Flux.

@RestController
@RequestMapping("/api/users")
public class UserController {
    private final UserService userService;
    public UserController(UserService userService) { this.userService = userService; }

    @GetMapping("/{id}")
    public Mono<User> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .switchIfEmpty(Mono.error(new UserNotFoundException(id)));
    }

    @GetMapping
    public Flux<User> getUsers(@RequestParam(defaultValue = "0") int page,
                               @RequestParam(defaultValue = "20") int size) {
        return userService.findAll(page, size);
    }
}

3. Implement a reactive service using ReactiveCrudRepository (R2DBC) for end‑to‑end non‑blocking data access.

@Service
public class UserService {
    private final ReactiveUserRepository repo;
    public UserService(ReactiveUserRepository repo) { this.repo = repo; }
    public Mono<User> findById(Long id) { return repo.findById(id); }
    public Flux<User> findAll(int page, int size) {
        return repo.findAll()
            .skip((long) page * size)
            .take(size);
    }
    public Mono<User> save(User u) { return repo.save(u); }
}

Without R2DBC, database calls remain blocking, negating the benefits of WebFlux.

WebClient – Reactive HTTP Client

WebClient replaces RestTemplate and can perform parallel external calls.

@Service
public class OrderService {
    private final WebClient client;
    public OrderService(WebClient.Builder b) {
        this.client = b.baseUrl("http://user-service")
                       .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                       .build();
    }
    public Mono<User> getUserWithOrders(Long userId) {
        Mono<User> userMono = client.get().uri("/api/users/{id}", userId)
                                 .retrieve().bodyToMono(User.class);
        Flux<Order> ordersFlux = client.get().uri("/api/orders?userId={id}", userId)
                                   .retrieve().bodyToFlux(Order.class);
        return userMono.zipWith(ordersFlux.collectList())
                       .map(t -> { t.getT1().setOrders(t.getT2()); return t.getT1(); });
    }
}

Parallel execution reduces total latency to the maximum of the individual calls, unlike sequential RestTemplate calls where latency adds up.

Why WebFlux Is Gaining Traction

Resource‑efficiency in cloud‑native environments: fewer pods, lower CPU and memory usage; throughput can increase 3‑5× under the same hardware.

Streaming & real‑time push: native support for Server‑Sent Events and WebSocket makes it ideal for financial tickers, IoT streams, and monitoring dashboards.

Back‑pressure: Reactive Streams let consumers signal producers to slow down, preventing overload in high‑throughput pipelines.

GraalVM native image compatibility: startup times can be reduced to under 100 ms, benefiting serverless and auto‑scaling scenarios.

Drawbacks & Suitable Scenarios

Steep learning curve: developers must master reactive concepts, operators, and back‑pressure.

Debugging complexity: stack traces span multiple operators; tools like BlockHound help detect blocking calls.

Ecosystem maturity: not all libraries have reactive versions; using blocking JDBC defeats the purpose.

Overkill for simple CRUD: the performance gain may not outweigh added complexity.

WebFlux shines in I/O‑intensive workloads (API gateways, microservice orchestration, streaming data processing). For CPU‑bound tasks or straightforward CRUD services, traditional Spring MVC combined with virtual threads may be more pragmatic.

Virtual Threads (Project Loom) – Complement, Not Replacement

Virtual threads allow synchronous code to achieve high concurrency with low overhead. Using Tomcat + virtual threads can match WebFlux’s throughput while preserving familiar imperative programming. However, WebFlux still holds advantages for fine‑grained back‑pressure, streaming, and native‑image scenarios.

Conclusion

If your application is I/O‑heavy, requires massive concurrency, or needs built‑in streaming capabilities, investing in WebFlux is worthwhile. For simple CRUD services, the combination of Spring MVC and virtual threads may be more efficient.

Official Resources

Spring WebFlux documentation: https://docs.spring.io/spring-framework/reference/web/webflux.html

Project Reactor documentation: https://projectreactor.io/docs

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

cloud nativeReactive ProgrammingVirtual ThreadsWebClientSpring WebFluxBackpressureR2DBC
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.