Overview
Reactive Streams is a specification (not an implementation) that standardizes async, non-blocking stream processing with backpressure. Project Reactor is Spring's implementation, providing Mono<T> (0-1 elements) and Flux<T> (0-N elements). The core value: a single thread can handle thousands of concurrent I/O operations by never blocking — it yields and resumes via callbacks, not threads.
Architecture
REACTIVE STREAMS SPECIFICATION
════════════════════════════════════════════════════════
Publisher<T> ──subscribe(Subscriber)──▶ Subscriber<T>
│
onSubscribe(Sub)
│
◀─── request(n) ─────────┘ ← BACKPRESSURE
│
onNext(item) × n
onNext(item)
onComplete() or onError()
Contract:
1. Subscriber requests N items (backpressure signal)
2. Publisher sends AT MOST N items
3. Ends with onComplete OR onError, never both
4. onNext never called after terminal signals
COLD vs HOT PUBLISHERS
════════════════════════════════════════════════════════
COLD Publisher:
├── Starts producing when subscribed
├── Each subscriber gets its own stream from the beginning
├── Examples: HTTP request, DB query, file read
└── Flux.fromIterable(), Mono.fromCallable()
HOT Publisher:
├── Produces regardless of subscribers
├── Subscribers join mid-stream (miss past events)
├── Examples: real-time events, stock prices, WebSocket feed
└── Flux.create() with SinkType.MULTICAST
PROJECT REACTOR THREADING MODEL
════════════════════════════════════════════════════════
Default: Everything runs on subscribe() thread (often main or HTTP worker)
publishOn(scheduler) → switches downstream operators to scheduler thread
subscribeOn(scheduler) → switches subscription and upstream to scheduler thread
Schedulers.boundedElastic() → for blocking I/O (JDBC, file system)
auto-scales thread pool, queue for excess
Schedulers.parallel() → for CPU work (N threads = N CPUs)
Schedulers.single() → single non-daemon thread (timer, coordination)
Schedulers.immediate() → current thread (default, no switch)
Technical Implementation
Backpressure with Custom Subscriber
// Manual subscriber demonstrating backpressure control
Flux.range(1, 1000)
.subscribe(new BaseSubscriber<Integer>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
request(10); // request first batch of 10 items
}
@Override
protected void hookOnNext(Integer value) {
System.out.println("Received: " + value);
if (value % 10 == 0) {
request(10); // request next batch when current batch processed
}
}
@Override
protected void hookOnError(Throwable ex) {
System.err.println("Error: " + ex.getMessage());
}
@Override
protected void hookOnComplete() {
System.out.println("Done");
}
});
Schedulers: Mixing Reactive and Blocking Code
@Service
public class UserService {
// WRONG: blocking call on reactive scheduler thread → starves thread pool
public Mono<User> findUserBad(String id) {
return Mono.just(jdbcRepo.findById(id)); // JDBC blocks the reactor thread!
}
// CORRECT: offload blocking work to boundedElastic thread pool
public Mono<User> findUser(String id) {
return Mono.fromCallable(() -> jdbcRepo.findById(id)) // wraps blocking call
.subscribeOn(Schedulers.boundedElastic()); // execute on I/O thread pool
}
// CPU-bound work on parallel scheduler
public Mono<byte[]> hashPassword(String raw) {
return Mono.fromCallable(() -> BCrypt.hashpw(raw, BCrypt.gensalt(12)))
.subscribeOn(Schedulers.parallel());
}
// Switching thread contexts in a pipeline
public Flux<UserSummary> getEnrichedUsers() {
return Flux.fromIterable(getIds()) // thread: subscriber's thread
.publishOn(Schedulers.boundedElastic()) // switch to I/O pool here
.flatMap(id -> Mono.fromCallable(
() -> jdbcRepo.findById(id) // runs on boundedElastic
))
.publishOn(Schedulers.parallel()) // switch to CPU pool here
.map(user -> toSummary(user)); // CPU work on parallel
}
}
Creating a Hot Publisher (Event Stream)
// Hot publisher: emits to all current subscribers
// New subscribers miss past events
Sinks.Many<OrderEvent> sink = Sinks.many().multicast().onBackpressureBuffer(256);
Flux<OrderEvent> hotStream = sink.asFlux();
// Producer — emits events whenever they occur
void publishOrderEvent(OrderEvent event) {
Sinks.EmitResult result = sink.tryEmitNext(event);
if (result.isFailure()) {
log.warn("Failed to emit event: {}", result);
}
}
// Multiple subscribers see the same events in real-time
hotStream.subscribe(event -> sendToWebSocket(userId1, event));
hotStream.subscribe(event -> writeToAuditLog(event));
// Convert hot to cold with replay (buffer last N events for new subscribers)
Flux<OrderEvent> replayableStream = hotStream.replay(10).autoConnect();
Interview Preparation
Q: What is backpressure and why does Reactive Streams need it?
Backpressure is a signal from the consumer to the producer to slow down. Without it: if a fast producer emits 1M items/second and a slow consumer processes 1K/second, you have two choices — buffer everything (memory explosion) or drop items (data loss). Reactive Streams solves this by making consumers explicit about capacity: subscription.request(n) tells the publisher "I can handle N items right now." The publisher must respect this. If the producer is backed by a network socket, it can stop reading (TCP receive window fills up → sender slows down). If it's a database cursor, it can pause fetching. Backpressure propagates all the way through the chain.
Q: What is the difference between publishOn and subscribeOn?
publishOn switches the downstream operators to the specified scheduler — everything after publishOn runs on that scheduler thread. subscribeOn switches the thread for the subscription chain itself — the upstream source and its initial execution run on that scheduler. In most pipelines with I/O sources, subscribeOn(Schedulers.boundedElastic()) is what you want: it moves the blocking source (JDBC, file read) onto an I/O thread, and everything downstream (operators, subscribers) runs wherever they end up. publishOn is used when you need to switch execution context mid-pipeline — e.g., do I/O on boundedElastic, then do CPU processing on parallel.
Q: What is the difference between cold and hot publishers?
A cold publisher creates a new, independent data stream for each subscriber and starts from the beginning. An HTTP call wrapped in Mono.fromCallable() is cold — each subscription makes a new HTTP request. A hot publisher exists independently of subscribers and emits events to all currently subscribed observers; late subscribers miss earlier events. A real-time WebSocket feed or an event bus is hot. In Reactor: Flux.create() with a Sink, Sinks.many().multicast(), and ConnectableFlux (from .publish()) are tools for creating hot publishers. Use .replay(n) to give new subscribers access to the last N events even though the stream is already running.
Learning Resources
- Project Reactor Reference — official docs
- Reactive Streams Specification — the underlying contract
- Reactor — Which operator? — operator selection guide
- Hands-On Reactive Programming in Spring 5 — Oleh Dokuka & Igor Lozynskyi