Frameworks and libraries today make it easy for programmers to adopt new paradigms, and reactive frameworks are no exception. However, just as an engineer must understand the principles of electricity before designing an alloy, a developer should grasp the key concepts of reactive programming before building serious applications. In this post, I’ll explain these core ideas and demonstrate them with practical examples.
To appreciate why reactive programming matters, let’s first revisit how communication typically works in object-oriented programming (OOP), where objects or processes interact either synchronously or asynchronously. In synchronous communication, the client waits for the server to respond before continuing. This is the traditional, blocking programming model: instructions are executed in order, and if a process needs to wait, it blocks the current thread until the result arrives.
The alternative is the non-blocking programming model, which uses asynchronous communication. Here, threads are never forced to wait. When a task must wait for a response, it is suspended, freeing the thread to handle other work. Once the response arrives, the task resumes—possibly on a different thread. This approach leads to better CPU utilization and scalability, especially under high load. Reactive frameworks help manage this complexity, making it easier to build efficient, non-blocking applications where threads are not forced to block.
In the context of reactive programming, these non-blocking interacting entities are often described using the terms publisher (or producer) and subscriber (or consumer), rather than the traditional client-server analogy. The consumer subscribes to data from the publisher, and the publisher emits data to all its subscribers.
One of the main challenges in this model arises when the publisher emits data faster than the subscriber can process it. Reactive programming addresses this with backpressure—a feedback mechanism where the subscriber signals to the publisher how many items it is ready to handle, keeping the system stable and efficient. Backpressure is especially critical in real-world scenarios such as processing large volumes of real-time data streams (e.g., IoT sensor data) or handling a high number of requests through an API gateway when the downstream database is too slow to keep up. Frameworks like RxJava and Project Reactor provide robust tools to manage backpressure and control data flow in reactive systems.
To see how this works in practice, let’s look at a simple example. We’ll implement a simulated slow consumer that only requests one item at a time. After processing each item, it explicitly requests the next. This approach demonstrates how a subscriber can manage backpressure by controlling the demand:
import com.example.entity.Employee;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Mono;
import java.time.Duration;
public class OneByOneEmployeeSubscriber extends BaseSubscriber<Employee> {
@Override
protected void hookOnSubscribe(Subscription subscription) {
System.out.println("Subscribed, requesting first employee");
request(1);
}
@Override
protected void hookOnNext(Employee employee) {
System.out.println("Received: " + employee);
Mono.delay(Duration.ofSeconds(10)) // simulate slow processing with delay
.doOnTerminate(() -> request(1)) // request next item only after processing has been done
.subscribe();
}
@Override
protected void hookOnError(Throwable throwable) {
System.err.println("Error: " + throwable);
}
@Override
protected void hookOnComplete() {
System.out.println("All employees processed.");
}
}
Note: In production-level code, a logging framework should be used instead of printing to standard output.
In this implementation, the subscriber manages backpressure by controlling the request rate, ensuring it processes data at its own pace and maintains system stability—even when the publisher emits data much faster. This pattern is especially useful in scenarios where processing is slow or resource-intensive.
In this article, we explored the core concepts of reactive programming, including the differences between synchronous and asynchronous models, the benefits of non-blocking execution, and the roles of publishers and subscribers. We also demonstrated how reactive systems manage data flow and backpressure, ensuring efficient and stable processing even when data is produced faster than it can be consumed.
