Reactive Programming

A programming paradigm centred on composable asynchronous data streams, where changes propagate automatically through a pipeline of operators — combining the Observer Pattern, the Iterator pattern, and functional programming.

Problem

Traditional asynchronous code (callbacks, futures/promises) composes poorly. Nested callbacks produce “callback hell.” Futures chain awkwardly. Neither provides a standard way to handle backpressure, cancellation, error propagation, or stream merging. As concurrency requirements grow, imperative async code becomes hard to reason about.

Solution / Explanation

Reactive Programming treats events, data, and errors as streams. A stream is a sequence of values over time. The developer composes transformations on streams using a rich library of operators — map, filter, flatMap, merge, zip, retry, timeout, etc. — rather than writing imperative control flow.

ReactiveX (Rx)

ReactiveX (reactivex.io), developed originally at Microsoft by Erik Meijer, is the most widespread family of reactive programming libraries. It is described as “a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming.”

Available as idiomatic implementations in: Java (RxJava), JavaScript (RxJS), .NET (Rx.NET), Scala, C++, Python, Clojure, Swift, Kotlin, and others.

Core Abstractions

Observable / Flowable A source of items emitted over time. Cold observables begin emitting when subscribed; hot observables emit regardless of subscriptions (e.g., UI events, sensor streams). In RxJava 2+, Flowable is a backpressure-aware Observable that implements Reactive Streams.

Observer / Subscriber Consumes items from an Observable. Provides three callbacks: onNext(item), onError(t), onComplete().

Operators First-class functions that transform streams:

CategoryExamples
Transformmap, flatMap, concatMap, scan
Filterfilter, take, skip, distinct, debounce
Combinemerge, zip, combineLatest, concat
Error handlingretry, onErrorResumeNext, catchError
Timingdelay, timeout, interval, throttle
Backpressurebuffer, window, sample, onBackpressureDrop

Schedulers Control which thread operators run on — Schedulers.io() for blocking I/O, Schedulers.computation() for CPU work, AndroidSchedulers.mainThread() for UI updates.

Project Reactor (Spring)

Project Reactor (projectreactor.io) is a fourth-generation reactive library fully compliant with Reactive Streams, designed for the JVM. It provides:

  • Flux<T>: a stream of 0–N elements.
  • Mono<T>: a stream of 0–1 elements (analogous to a non-blocking Future).

Reactor is the reactive foundation of Spring WebFlux, Spring’s non-blocking web framework introduced in Spring 5.

Reactive vs. Imperative Async

AspectImperative Async (futures/callbacks)Reactive (Rx / Reactor)
ComposabilityLow (callback nesting)High (operator chains)
Error propagationManual, easy to missBuilt into stream (onError)
BackpressureNoneFirst-class (request(n))
CancellationManualDisposable.dispose() / cancel()
Thread managementExplicitVia Schedulers

Key Components / Rules

  • Streams are lazy — nothing happens until someone subscribes.
  • Immutable pipelines — each operator returns a new stream; the original is unchanged.
  • Error channels are first-class — errors flow through the stream and can be handled with operators like onErrorReturn.
  • Prefer Flowable over Observable in RxJava 2+ for any potentially high-volume stream to enable backpressure.
  • Avoid blocking in reactive pipelines — blocking calls inside map starve the event loop; use subscribeOn(Schedulers.io()) to offload.

When to Use

  • Non-blocking I/O — HTTP servers, database drivers (R2DBC), WebSockets.
  • Event-driven UI (RxJS in Angular/React).
  • Real-time data pipelines (sensor streams, financial ticks).
  • Systems that must compose multiple async data sources.

Not a good fit when:

  • Simple request/response with no concurrency needs (a plain future or synchronous call is clearer).
  • Team has no reactive experience — the learning curve is steep.

Trade-offs

BenefitCost
Composable async pipelinesHigh learning curve; stack traces are hard to read
Backpressure built inDebugging complex operator chains is difficult
Consistent error handlingBlocking calls accidentally introduced destroy performance
Rich operator libraryOperator overload — many ways to do the same thing