Create your own
Lesson illustration

Reactive Programming Fundamentals

Hello! Welcome to our next lesson in the "Advanced Concurrency & Performance" module.

In our last session, we dove into the world of deadlocks, a critical problem within the traditional thread-per-request concurrency model. We saw how blocking for locks can bring a system to a halt and learned strategies to prevent it. This model, where each request holds onto a thread while waiting for I/O (like database calls or other microservice responses), has fundamental scalability limits. As you scale, you need more and more threads, which consumes memory and increases the overhead of context switching.

Today, we're going to explore a completely different approach to concurrency. Our learning outcome is to explain the core principles of reactive programming, including non-blocking I/O and backpressure. This paradigm is the foundation for building highly responsive and resilient systems that can handle massive concurrency with very few resources. For senior-level interviews, explaining the why behind reactive programming is just as important as knowing how to implement it.

Let's get started.

1. The Problem: Why Traditional Blocking Is Wasteful

In a typical Spring MVC application, when a request comes in, a thread from a large thread pool is assigned to handle it. If that handler needs to query a database, the thread blocks—it sits idle, consuming memory and waiting for the database to respond.

To understand why this is a problem, let's explore the core issue of resource waste.

Introduction to Reactive Programming

The official documentation for Project Reactor (the library behind Spring WebFlux) has an excellent introduction that starts by explaining the limitations of the blocking model.

Please read section '1. Blocking Can Be Wasteful'. It clearly explains how latency from I/O operations leads to idle threads and wasted resources, setting the stage for why a different approach is needed.

This wastefulness becomes a major bottleneck under high load. If all threads in your pool are blocked waiting for slow I/O, your application can't accept new requests, leading to poor performance and cascading failures.

To see this in action, let's watch a practical demonstration. The following video first builds a traditional, blocking REST endpoint in Spring Boot.

Spring Boot WebFlux | Asynchronous and Non Blocking Reactive Programming | Example | Javatechie

This video by Java Techie provides a clear, practical comparison between a traditional blocking endpoint and a reactive one. For now, we'll focus on the blocking implementation to see the problem firsthand.

Watch from the beginning up to 13:27. The presenter creates a standard REST controller that simulates a 1-second delay for each of the 50 customer records it fetches. Observe how the server logs each processing step, but the client (the browser) gets no response until the entire 50-second operation is complete. This is the blocking model in action.

As you saw, the client is stuck waiting for the full payload. The server is busy, but from the user's perspective, nothing is happening for a long time. This is the user experience we want to avoid.

2. The Reactive Paradigm: Reacting to Events and Data Streams

Reactive programming offers a solution. Instead of blocking, we build systems that react to events as they happen—like data arriving from a network call or a database query completing.

At its core, reactive programming is a declarative paradigm focused on data streams and the propagation of change.

02 What is reactive programming (Reactive programming with Java - full course)

To get a solid conceptual foundation, let's watch this explanation from Java Brains.

Watch the segments from 00:00 to 02:50 and then from 04:59 to 06:58. The video defines reactive programming, clarifies that it's not the same as asynchronous programming, and most importantly, addresses why a backend Java developer should care about it by contrasting it with the synchronous request-response model you're familiar with.

The key shift is thinking about data not as a static collection you fetch, but as a dynamic stream of events you react to over time.

3. The Core Principles

To make this paradigm work, we rely on three interconnected principles: non-blocking I/O, the Publisher-Subscriber model for data streams, and backpressure.

Principle 1: Non-Blocking I/O and the Event Loop Model

The foundation of a reactive system is non-blocking I/O. When a reactive application makes a network call, it doesn't wait. The call returns immediately, and the thread is freed up to handle other work. When the data is ready, the system sends a notification (like a callback), and the processing continues.

This allows a small, fixed number of threads (called an event loop) to handle a massive number of concurrent requests.

Overview :: Spring Framework

The Spring documentation provides a fantastic explanation of this concurrency model shift, which is critical for someone with your Spring MVC background to understand.

Read the introductory section 'Why was Spring WebFlux created?' and then jump to the 'Concurrency Model' section. Pay close attention to the contrast between the large thread pool in Spring MVC and the small, fixed-size event loop in Spring WebFlux. This is a crucial concept for interviews.

Here is a visual representation of how a non-blocking server like Netty (which Spring WebFlux uses by default) handles requests using an EventLoop.

Reactor Netty HTTP Server Architecture with Spring WebFlux
This diagram shows how a non-blocking server processes requests. A small number of EventLoop threads handle I/O events from many connections. When an operation like a database query is initiated, the EventLoop thread doesn't block; it moves on to other tasks. The response from the database is handled as a new event in the queue when it arrives.

Now, let's see the payoff. Watch the second half of the Java Techie video where he implements and runs the reactive version of the same endpoint.

Spring Boot WebFlux | Asynchronous and Non Blocking Reactive Programming | Example | Javatechie

Continuing with the same video, let's see the reactive solution.

Watch from 13:27 to 22:28. The presenter refactors the code to use Spring WebFlux and a Flux, which represents a stream of customers. Notice how when he hits the new endpoint, the browser starts receiving and displaying customer data immediately, one by one, as they are processed. The total time is the same, but the perceived performance is dramatically better. This is the power of non-blocking, streaming responses.

Principle 2: Data Streams (Publisher-Subscriber)

You saw the code change from returning a List<Customer> to a Flux<Customer>. Flux (and its counterpart Mono, for 0 or 1 items) are the core building blocks in Project Reactor. They are implementations of the Publisher type from the Reactive Streams specification.

In this model:

  • A Publisher emits a sequence of events (0 to N items).
  • A Subscriber listens to these events.
  • The communication contract is defined by three types of signals the Publisher can send: onNext (a data item), onComplete (end of stream), and onError (an error occurred).

This entire process is "lazy." You define a chain of operations on the stream (like a recipe), but nothing happens until a Subscriber actually subscribe()-s to the Publisher, triggering the data flow.

Introduction to Reactive Programming

The Project Reactor documentation explains this 'assembly line' analogy and the Publisher-Subscriber contract well.

Read the introductory section up to '1. Blocking Can Be Wasteful', then sections '3. From Imperative to Reactive Programming' and '3.4. Nothing Happens Until You subscribe()'. This will formalize your understanding of the Publisher-Subscriber model and the declarative nature of reactive code.

Principle 3: Backpressure

We've established that the publisher pushes data to the subscriber. But what if the publisher is a firehose and the subscriber is trying to drink from it with a straw? A fast publisher can easily overwhelm a slow consumer, causing it to run out of memory and crash.

This is where backpressure comes in. It's a crucial feedback mechanism that allows the subscriber to signal to the publisher how much data it's ready to handle.

Overview :: Spring Framework

The Spring and Reactor docs both offer concise explanations of this critical concept.

First, read the sections 'Define “Reactive”' and 'Reactive Streams' in the Spring documentation. It clearly connects backpressure to the definition of 'reactive'.

The Reactive Streams specification formalizes this with the Subscription object. When a Subscriber subscribes to a Publisher, it receives a Subscription. It then uses subscription.request(n) to signal that it's ready to process n elements. This transforms the pure "push" model into a "push-pull" hybrid.

Reactive Streams Interfaces UML Diagram
This UML diagram shows the core interfaces of the Reactive Streams specification. Notice the `Subscription` interface with its `request(long n)` method. This is the mechanism for the Subscriber to apply backpressure and control the flow of data from the Publisher.

Introduction to Reactive Programming

For a deeper dive into the mechanism, let's return to the Reactor documentation.

Now, read section '3.5. Backpressure'. It explains the request(n) mechanism and how it creates a 'push-pull hybrid' model, giving the consumer control.

The ability for a client to cancel a stream, as briefly shown at the end of the Java Techie video (22:28 - 24:14), is an extreme form of backpressure—it's essentially the client saying request(0) and telling the server to stop sending data, which prevents wasted work on the server.

Test your understanding! (Interview Question)

You're working on a high-throughput microservice built with Spring WebFlux. It's running on a server with 8 CPU cores. A colleague observes that the service is only using 8 threads for processing requests and suggests increasing the size of the Netty event loop thread pool to 100 to improve performance.

What is your assessment of this suggestion? Why is it likely the wrong approach, and what could be the real cause of performance issues in a reactive application?

Show answer

Assessment: The suggestion to dramatically increase the number of event loop threads is almost always incorrect and indicates a misunderstanding of the reactive concurrency model.

Explanation: The power of the reactive model comes from using a small, fixed number of threads (typically one per CPU core) that never block. These threads are designed to quickly handle I/O events and delegate tasks, not to perform long-running or blocking work.

Increasing the thread count from 8 to 100 won't help because the bottleneck in a properly written reactive application is not a lack of threads; it's the CPU or I/O capacity itself. Adding more threads just adds unnecessary overhead from context switching.

The Real Cause: If a reactive service is slow, the most likely culprit is an accidental blocking call hidden somewhere in the reactive pipeline. For example, a developer might have called a traditional JDBC driver, Thread.sleep(), or a blocking HTTP client from within a map or flatMap operator. When a blocking call is made on an event loop thread, that thread is tied up and cannot serve any other requests, effectively starving the entire event loop and destroying the benefits of the reactive model.

The correct approach to fixing the performance issue is to profile the application, find the blocking call (using tools like BlockHound or a profiler), and replace it with its non-blocking equivalent (e.g., using a reactive database driver like R2DBC, or WebClient for HTTP calls).

Conclusion

Today, we've unpacked the foundational theory of reactive programming. This shift in thinking is essential for building the next generation of scalable microservices.

Key Takeaways:

  • Motivation: Reactive programming solves the inefficiency of the traditional blocking model, where threads are wasted waiting for I/O.
  • Non-Blocking I/O: The core mechanism that allows a small number of threads (an "event loop") to handle massive concurrency by never waiting for I/O to complete.
  • Data as Streams: We model data as streams of events over time using Publisher types (Flux, Mono). We compose logic declaratively using operators, and nothing happens until a Subscriber subscribes.
  • Backpressure: The critical safety valve. It's a feedback mechanism that allows a slow consumer to control the rate of a fast producer, preventing it from being overwhelmed.

Next Up

We've covered the "what" and the "why." In our next lesson, we will get hands-on with the "how." You will implement non-blocking REST controllers using Spring WebFlux (Mono/Flux) for high-concurrency scenarios. We'll take the concepts from today and turn them into production-ready code.

Can't find a good explanation? Sign up and we'll make it for you

Sign up