Reactive programming is a paradigm focused on asynchronous data flow, non-blocking execution, and responsive systems that can handle data streams dynamically and efficiently. Unlike traditional programming models that are often synchronous and blocking, reactive programming embraces events and streams of data that can be processed as they arrive, enabling highly scalable and resilient applications.
To standardize reactive programming in Java, the Reactive Streams specification defines a minimal API to handle asynchronous stream processing with backpressure. This specification is the foundation for many reactive libraries and is included as the java.util.concurrent.Flow
API in Java 9+.
Synchronous Model | Reactive Streams Model |
---|---|
Caller requests data and waits (blocking). | Data flows asynchronously without blocking. |
Data is pulled on demand or pushed without control. | Backpressure allows subscriber to control demand. |
Sequential, step-by-step execution. | Event-driven, asynchronous processing pipeline. |
Publisher ---> Subscription ---> Subscriber
| ↑ |
| | request(n) |
|--------------> |-----------------|
| | |
| emit data (onNext) |
|--------------> |-----------------|
Subscription
.In essence, reactive programming transforms how we deal with data streams by making them asynchronous, non-blocking, and controllable through backpressure, enabling systems that are highly responsive and resilient to varying workloads. This new paradigm addresses the limitations of traditional synchronous or event-driven push/pull models by explicitly coordinating the data flow between producers and consumers.
Java Streams and Reactive Streams both deal with processing sequences of data, but they differ fundamentally in design, execution model, and use cases. Understanding these differences helps choose the right tool for your needs.
Feature | Java Streams | Reactive Streams |
---|---|---|
Model | Pull-based (consumer requests data) | Push-based (producer pushes data) |
Execution | Synchronous and blocking | Asynchronous and non-blocking |
Data Size | Finite, typically eager processing | Potentially infinite or unbounded |
Evaluation | Lazy intermediate ops, eager terminal op | Fully asynchronous, event-driven |
Error Handling | Errors thrown immediately, stopping the pipeline | Errors propagated asynchronously through callbacks |
Backpressure Support | None (consumer pulls at its own pace) | Built-in backpressure controls data flow |
Lifecycle | Single-use pipeline, terminates after terminal op | Long-lived, supports subscription, cancellation, multiple events |
Parallelism | Supports parallel streams via parallel() |
Designed for concurrent, non-blocking streams |
Pull vs Push: Java Streams pull elements on demand during terminal operations. Reactive Streams push data asynchronously as it becomes available, requiring Subscribers to signal readiness via backpressure.
Finite vs Potentially Infinite: Java Streams are designed for finite data sources (collections, arrays). Reactive Streams can handle infinite data flows like sensor data, user events, or live feeds.
Error Handling: In Java Streams, exceptions interrupt the flow immediately. Reactive Streams propagate errors through the onError
callback, allowing subscribers to react gracefully.
Lifecycle and Cancellation: Reactive Streams provide Subscription
that allows subscribers to cancel or limit data flow. Java Streams terminate naturally after processing all elements.
Java Streams (Synchronous, Pull-based):
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
System.out.println("Sum of even numbers: " + sum);
Reactive Streams (Flow
API, Asynchronous, Push-based):
import java.util.concurrent.SubmissionPublisher;
import java.util.concurrent.Flow.*;
public class ReactiveExample {
public static void main(String[] args) throws InterruptedException {
SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();
Subscriber<Integer> subscriber = new Subscriber<>() {
private Subscription subscription;
private int sum = 0;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1); // Request one item
}
@Override
public void onNext(Integer item) {
if (item % 2 == 0) {
sum += item;
}
subscription.request(1); // Request next item
}
@Override
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void onComplete() {
System.out.println("Sum of even numbers: " + sum);
}
};
publisher.subscribe(subscriber);
// Publish numbers asynchronously
for (int i = 1; i <= 5; i++) {
publisher.submit(i);
}
publisher.close();
Thread.sleep(100); // Give time for async processing
}
}
Aspect | Java Streams | Reactive Streams |
---|---|---|
Data Flow | Pull-based, synchronous | Push-based, asynchronous |
Data Source | Finite collections/arrays | Potentially infinite streams |
Execution Model | Lazy intermediates, eager terminal | Fully async with backpressure |
Error Handling | Immediate exceptions | Asynchronous error callbacks |
Lifecycle | Single use pipeline | Long-lived, subscribable stream |
Use Case Examples | Batch data processing | Event-driven, live data streams |
Java Streams excel in simple, synchronous batch processing, while Reactive Streams provide powerful tools for asynchronous, event-driven, and scalable stream processing—especially suited to modern distributed and interactive systems.
Flow
API (Java 9+)Java 9 introduced the java.util.concurrent.Flow
API to provide a standard interface for reactive streams. This API defines four key interfaces:
Here, we'll focus on implementing a simple reactive stream pipeline by creating a custom Publisher
and Subscriber
and connecting them.
onNext()
, onError()
, and onComplete()
methods.StringPublisher
and LoggingSubscriber
This minimal example demonstrates a publisher sending a fixed list of strings to a subscriber that logs received messages.
import java.util.List;
import java.util.concurrent.Flow.*;
import java.util.concurrent.SubmissionPublisher;
public class SimpleFlowExample {
public static void main(String[] args) throws InterruptedException {
// Publisher that asynchronously submits strings
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
// Subscriber that logs received items
Subscriber<String> subscriber = new Subscriber<>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1); // Request the first item
}
@Override
public void onNext(String item) {
System.out.println("Received: " + item);
subscription.request(1); // Request next item after processing
}
@Override
public void onError(Throwable throwable) {
System.err.println("Error occurred: " + throwable.getMessage());
}
@Override
public void onComplete() {
System.out.println("All items processed.");
}
};
// Connect subscriber to publisher
publisher.subscribe(subscriber);
// Publish some items asynchronously
List.of("Hello", "Reactive", "Streams", "with", "Flow API")
.forEach(publisher::submit);
// Close the publisher to signal end of stream
publisher.close();
// Wait briefly to allow async processing to complete
Thread.sleep(1000);
}
}
Explanation:
SubmissionPublisher
is a built-in Publisher that handles asynchronous submission.Subscriber
implements the 4 required methods.onSubscribe()
, the Subscriber requests one item.onNext()
, it processes and requests the next.onComplete()
signals the stream has ended.Thread.sleep()
ensures the main thread waits for async processing.To understand internals, here is a minimal custom Publisher and Subscriber illustrating manual data flow control.
import java.util.concurrent.Flow.*;
public class ManualFlowExample {
public static void main(String[] args) {
Publisher<String> publisher = new Publisher<>() {
@Override
public void subscribe(Subscriber<? super String> subscriber) {
subscriber.onSubscribe(new Subscription() {
private boolean completed = false;
@Override
public void request(long n) {
if (!completed) {
subscriber.onNext("Custom");
subscriber.onNext("Flow");
subscriber.onNext("Example");
subscriber.onComplete();
completed = true;
}
}
@Override
public void cancel() {
System.out.println("Subscription cancelled");
}
});
}
};
Subscriber<String> subscriber = new Subscriber<>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(1);
}
@Override
public void onNext(String item) {
System.out.println("Received: " + item);
subscription.request(1);
}
@Override
public void onError(Throwable throwable) {
System.err.println("Error: " + throwable.getMessage());
}
@Override
public void onComplete() {
System.out.println("Processing complete.");
}
};
publisher.subscribe(subscriber);
}
}
Explanation:
Publisher
calls subscriber.onSubscribe()
with a Subscription
implementation.request(long n)
method pushes predefined data items on demand.Subscriber
requests one item at a time, processing and then requesting the next.Flow
API provides a simple but powerful abstraction for asynchronous, non-blocking stream processing.SubmissionPublisher
helps create async publishers with minimal code.With these basics, you can build scalable reactive data flows integrated into modern Java applications.