Historical Coincidence: Why Did WebFlux Choose Complex Reactive Programming?
Jul 9, 2025 · 2044 words
Recently, the popularity of the Spring WebFlux framework has been skyrocketing. It is said to be capable of building high-performance applications with millions of concurrent connections. Many new projects, such as Spring AI, are primarily promoting Spring WebFlux instead of the classic Spring MVC.
However, when you start using Spring WebFlux, you discover that it forces you to use constructs like Mono and Flux, and it often throws strange errors. After some research, you learn that this is called "Reactive Programming"—a concept that sounds fresh and cool but feels incredibly confusing to learn.
This inevitably leads to a major question: Why must I adopt such a complex and difficult programming paradigm just to write a high-performance application?
If you share this doubt, please follow this article as we explore the mystery behind the birth of WebFlux. We will travel back to the crossroads where Spring WebFlux was born, examining the gains and losses in the evolution of the Java language and the engineering choices made by the WebFlux team.
The Complexity of Reactive Programming
Reactive programming is a paradigm that sounds beautiful in theory but is notoriously complex in practice.
When you look up the concept, you'll find it described as a declarative programming paradigm oriented around asynchronous data streams. You can imagine it as a highly automated "data pipeline." In this pipeline, data is not processed in large chunks but flows through one element at a time.
The implementation of reactive programming in Java is Project Reactor, which is the core dependency of WebFlux. It provides two powerful "conveyor belts": Mono for transporting 0 or 1 data item, and Flux for transporting 0 to N data items.
You can set up various workstations—known as operators—along the pipeline, such as map and flatMap, to transform, filter, and combine the flowing data in a declarative way. This pipeline also features a critical backpressure mechanism. If the downstream processing speed cannot keep up, it can signal the upstream to slow down, ensuring the system isn't overwhelmed under high pressure.
It all sounds wonderful, right? But this beauty comes at a price. Have you encountered these frustrations while trying WebFlux?
- You want to write simple business logic, but you end up wrestling with
MonoandFluxfor hours, feeling like you're using a sledgehammer to crack a nut. - A simple call chain gets wrapped in layers of
mapandflatMap; two weeks later, you can no longer understand the code you wrote. - Once it finally runs, you hit a bug and see an impossibly long stack trace. Finding the root cause feels harder than climbing to the stars.
However, the most fatal issue is "Function Coloring."
Reactive code is highly infectious. Once one of your methods returns a Mono or Flux, the method calling it—and all subsequent callers—are forced to become reactive. This chain of "color" spreads to every corner of your codebase. You must use reactive database drivers like R2DBC and the reactive WebClient, abandoning familiar tools like JDBC and RestTemplate. This imposes massive learning and maintenance costs on a project.
Why Did Spring WebFlux Emerge?
If reactive programming is so complex, why does Spring WebFlux even exist? The answer is simple: Spring MVC truly struggles with high-concurrency scenarios. Facing new challenges, Java needed a new web framework.
Classic Spring MVC is built on a simple and intuitive model: "one thread per request." Whenever a user request comes in, a Servlet container like Tomcat pulls a thread from a thread pool to serve that specific request. This model is straightforward; the request thread is bound to an operating system thread, making it very stable.
However, with the rise of microservices and cloud-native applications, we need to handle thousands or even millions of concurrent connections simultaneously. This is where Spring MVC's blocking I/O reveals its fatal flaw. When code executes an I/O operation (like querying a database or calling an API), the current thread is blocked. Thousands of blocked threads lead to massive resource waste, and server performance quickly hits a bottleneck.
Spring WebFlux was born precisely to break this "one thread per request" model, using a small number of threads to support a large number of user requests.
The core philosophy of Spring WebFlux draws inspiration from the Event Loop model of Node.js. Node.js was a highly innovative invention that used a single thread to support all concurrent requests, achieving extreme resource efficiency through non-blocking I/O. WebFlux adopted this concept but expanded the single thread into a multi-threaded event loop to better utilize the multi-core CPUs of modern servers.
At its base, Spring WebFlux uses Netty, a high-performance web server. Netty is the de facto standard for high-performance asynchronous network programming in the Java world. It provides all the low-level tools needed to build an event loop. However, Netty's API is too low-level, making direct development inefficient. WebFlux cleverly wraps this powerful engine, providing a friendlier high-level API that allows developers to focus on business logic without worrying about low-level networking details.
The Crossroads of History: Why WebFlux Chose Reactive Programming
WebFlux borrowed ideas from Node.js and used Netty as its foundation. Everything was ready except for one thing: a programming model that could elegantly glue it all together. Unfortunately, at that time, Java's language capabilities were actually holding it back.
Let’s look back at the historical timeline to see what choices the Spring team faced at that critical crossroads.
Vertically, Java itself was evolving slowly:
- 2013: Netflix released RxJava, bringing reactive programming concepts into the mainstream Java community. While powerful, it was born before Java 8, lacked Lambda support, and had cumbersome syntax.
- 2014: Java 8 was released, bringing Lambda expressions. Java finally gained a crucial modern language feature, simplifying the syntax for many libraries and frameworks.
- 2015: The Reactive Streams specification was officially released. This provided a unified standard for asynchronous stream processing on the JVM, but it was just an interface specification, not a concrete implementation.
- 2016: Project Reactor released its pivotal version 2.0, fully complying with the Reactive Streams specification and laying a solid foundation for the upcoming Spring 5 and WebFlux.
However, Java still lacked a key feature: Coroutines (user-level threads). Java's virtual threads (Project Loom) were not officially released until Java 21 in 2023.
Horizontally, other languages already had flourishing coroutine capabilities:
- Go: Go 1.0, released as early as 2012, had built-in Goroutines. By 2016, it was already a mature solution for building high-concurrency services. The syntax for Goroutines was incredibly simple; developers could write code in a synchronous style and get powerful asynchronous execution.
- Node.js: The Node.js community, long plagued by "callback hell," finally saw the official landing of
async/awaitsyntax in early 2017. This greatly simplified asynchronous code at the language level. - Kotlin: As an emerging language on the JVM, Kotlin introduced coroutines as an experimental feature in March 2017. It signaled that exploration for better concurrency models within the JVM ecosystem had already begun.
Now, let's look at the situation the Spring team faced. They were looking at three clear facts:
- The problem was identified: Spring MVC's blocking model had severe scalability bottlenecks under high concurrency.
- The underlying solution existed: Netty could solve the problem, but it was too complex; a modern web framework had to be built on top of it.
- Language-level asynchronous capability was lacking: Java had neither transparent coroutines like Go nor syntax sugar like
async/await. Forcing developers to write manual callback functions would have been a disaster.
Under these constraints, the Spring team's choice became exceptionally clear. They needed a solution that could organize asynchronous logic, avoid callback hell, and be robust enough for production.
Looking around, the Reactive Streams specification (standardized in 2015) and its implementation library, Project Reactor, were the only rational and feature-complete options available. It not only solved callback hell through method chaining but also brought a powerful set of stream processing solutions with built-in backpressure.
This is the core logic behind the birth of WebFlux. It is essentially a "framework-level shim." When a language lacks a key capability, the framework steps up to simulate it. WebFlux uses framework complexity to fill the gap left by the JVM's lack of lightweight concurrency.
Coroutines: Java's Achilles' Heel
Why did the absence of coroutines have such a massive impact? We need to look at how other languages handled this and where Java stood at the time.
The Ideal Coroutine Implementation: Goroutines
Go was built with Goroutines from the start. They allow developers to use the simplest, most intuitive synchronous blocking code to achieve asynchronous non-blocking performance. Developers don't need to worry about thread switching or callbacks; they just write code normally to create high-concurrency programs. This is the ideal model many Java developers dreamed of.
Goroutines are known as stackful coroutines. Each Goroutine has its own independent, tiny, and dynamically growing stack. The Go runtime scheduler manages the context of Goroutines and cleverly schedules a massive number of them across a small number of physical threads. All of this is transparent to the developer, creating the "magic" of writing asynchronous logic with synchronous code.
The Second-Best Model: async/await
Languages like JavaScript and Python provide async/await. While it still suffers from issues like function coloring and unfriendly stack traces, it at least allows developers to write asynchronous code in a way that looks synchronous, greatly improving the developer experience.
async/await is known as stackless coroutines. It is essentially syntax sugar that converts Promises into a synchronous-looking style. The compiler is responsible for weaving different async functions together. Therefore, async/await does not have a real call stack at runtime. This is why the async keyword "colors" the entire call chain—the compiler needs that explicit information to perform the transformation.
Java's Situation at the Time
Why didn't Java coroutines appear back then? Behind this lay heavy historical and technical baggage. From its inception, Java made a far-reaching design decision: to bind java.lang.Thread one-to-one with operating system kernel threads. This model provided excellent stability and a simple mental model for concurrency at the time, but it also meant that Java's entire concurrency ecosystem—including the memory model, the synchronized keyword, and JNI (Java Native Interface)—was built around these heavyweight kernel threads.
Introducing lightweight user-space coroutines on top of this foundation was equivalent to performing "open-heart surgery" on the JVM. It required rewriting the scheduler, overhauling memory management, and handling interactions with native code—a massive engineering undertaking. The Java community's pursuit of extreme stability and backward compatibility also meant that such a fundamental change had to undergo years of cautious research and development, which eventually became Project Loom.
The lack of language-level coroutine support left Java developers in a painful dilemma:
- A. Simple but Inefficient: Stick with Spring MVC. The code is simple, but the application's performance ceiling is low.
- B. Efficient but Complex: Embrace Spring WebFlux. It supports high concurrency, but you must endure all the complexity of reactive programming.
This was Java's biggest pain point at the time, leading many systems to seek solutions outside of Java.
Looking Back at History, Moving Toward the Future
Now, we can answer the original question.
WebFlux chose reactive programming not because it was the ultimate goal of programming, but because at that specific point in history, it was the only viable path for the Spring team to achieve high-efficiency concurrency. It was a brilliant, yet compromise-filled solution designed under the constraints of limited platform capabilities.
As the wheels of history turned, Java finally delivered its own answer: Virtual Threads in Java 21.
Java's Virtual Threads are very similar to Goroutines. Their arrival allows traditional Spring MVC applications to gain massive concurrency capabilities almost "for free" with just a configuration toggle, without major code changes. From the JVM level, it fundamentally resolves the core conflict that plagued Java developers for nearly a decade.
This inversely proves our point. When a language fixes its own shortcomings, the necessity of framework-level workarounds diminishes. Spring WebFlux will eventually return to the domains it should focus on: true stream processing and event-driven architectures.
For the vast majority of Java developers, the era of struggling to choose between simplicity and efficiency is over.