Reactive Programming

Reactive programming models computation as streams of values that flow through composed operators. Async, push-based, with explicit handling of backpressure (producer outpaces consumer). RxJS, Reactor (Java), Akka Streams, ReactiveX family.

The model is powerful for specific situations — high-throughput streaming, complex async event composition, backpressure-sensitive pipelines. For everything else, async/await is usually simpler and equally good.

The mental model

In imperative async code:

```

result = await fetchData(url)

processed = transform(result)

return await save(processed)

```

In reactive:

```

fetchData(url)

.map(transform)

.flatMap(save)

.subscribe(result => ..., error => ...)

```

A stream emits zero or more values; operators (`map`, `filter`, `flatMap`) transform; subscribers consume. Async by default; composable.

When reactive wins

- **Streams of events.** UI events, message bus, sensor data — naturally a stream.

- **Multiple async sources combining.** "Wait for all of these; on each, transform; combine; emit." Reactive operators (`combineLatest`, `merge`, `zip`) handle this elegantly.

- **Backpressure-critical pipelines.** Producer faster than consumer; reactive streams have a defined protocol for the consumer to signal ready.

- **Transformations that compose.** Map, filter, debounce, throttle, retry, switchMap as a chain.

- **UI reactivity.** RxJS in Angular; combineLatest of multiple stores into a derived UI state.

When async/await wins

- **Sequential async work.** "Fetch this, then that, then save." `await` reads top-to-bottom; reactive's chain reads similarly but is more verbose.

- **Single-shot operations.** Not a stream; just one async call. No reason for reactive overhead.

- **Most of normal application code.** A REST handler calling a database and returning. async/await is enough.

The 2020s consensus: reactive is overused. For most async code, async/await with proper error handling is simpler.

The 5 things reactive does that async/await doesn't

1. Backpressure

Async/await: producer awaits consumer's response, then produces next. Implicit one-at-a-time.

Reactive: producer can produce continuously; consumer requests N items; if producer is faster, it can buffer, drop, or apply policy.

```javascript

source$.pipe(

bufferCount(100), // batch in 100s

concatMap(batch => process(batch)) // wait for one before next

)

```

For streams where production rate genuinely outpaces consumption rate, this matters.

2. Time-based operators

```javascript

source$.pipe(

debounceTime(300), // emit only after 300ms of silence

distinctUntilChanged(),

switchMap(query => searchApi(query))

)

```

Standard auto-complete. The combination of `debounce`, `distinct`, `switchMap` (cancels previous on new emission) is hard to write cleanly with async/await.

3. Cancellation propagation

When a subscriber unsubscribes, the upstream operations cancel. AbortController in browser fetch is the equivalent for plain async; reactive bakes it in.

4. Composing multiple streams

```javascript

combineLatest([userStore$, settingsStore$, currentRoute$]).pipe(

map(([user, settings, route]) => deriveState(user, settings, route))

)

```

Re-emits whenever any of the sources emit. Async/await would require manual coordination.

5. Hot vs cold streams

A cold stream replays its values for each subscriber. A hot stream is shared; multiple subscribers see the same emissions.

```javascript

const sharedClicks$ = clicks$.pipe(share()); // hot

```

Useful for caching expensive computations; for multicasting events.

Backpressure strategies

When producer outpaces consumer, what happens?

- **`buffer`** — accumulate; risk OOM if producer is too fast.

- **`drop`** — discard new items.

- **`drop_oldest`** — keep new; discard old.

- **`error`** — fail loudly when buffer is full.

- **`block` (in some implementations)** — pause producer until consumer catches up.

Reactive frameworks make this explicit. Pick a policy per stream.

Common operators

| Operator | What it does |

|---|---|

| `map` | Transform each value |

| `filter` | Keep matching values |

| `flatMap` / `mergeMap` | Transform to a stream; flatten; concurrent |

| `concatMap` | Transform to a stream; flatten; sequential |

| `switchMap` | Transform to a stream; cancel previous on new |

| `debounceTime` | Emit only after silence |

| `throttleTime` | Emit at most once per period |

| `distinctUntilChanged` | Drop consecutive duplicates |

| `take` | Take first N |

| `takeUntil` | Stop on signal |

| `combineLatest` | Combine latest from multiple sources |

| `zip` | Combine pairs |

| `merge` | Interleave |

| `concat` | Sequential |

| `share` | Multicast |

| `retry` | Retry on error |

| `catchError` | Handle errors |

The vocabulary is extensive. Most production reactive code uses 10-15 operators heavily; the rest are edge-case-only.

Anti-patterns

- **Reactive everywhere.** Wrapping plain async calls in observables. Adds verbosity; no win.

- **Side effects in `map`.** `map` should be pure; side effects in `tap` (named explicitly).

- **Subscribing without unsubscribing.** Memory leak; subscriptions hold reference to source.

- **`.toPromise()` everywhere.** Converting back to promise immediately. Stick to one paradigm.

- **Nested `flatMap`.** Hard to read; refactor to chains.

- **Imperative state outside the stream.** Defeats the model; mix carefully.

Where reactive shines in 2026

- **Frontend UIs** — RxJS in Angular, signal-based reactivity in newer frameworks (SolidJS, Vue 3). The model fits user-event-driven UIs well.

- **Streaming backends** — Reactor (Java), specifically WebFlux / Spring Reactive, for high-throughput non-blocking servers.

- **Event-driven backends** — Akka Streams in Scala / Java; backpressure-aware event pipelines.

- **Bridging async sources** — composing data from multiple sources reactively.

Where reactive has lost ground

- **In React** — moved away from RxJS. Hooks + context + signals (in Solid-like) cover most of what RxJS did.

- **Node.js general async** — async/await dominates; RxJS is one of several streaming options.

- **Java backends** — Project Loom (virtual threads, Java 21+) makes most reactive Java unnecessary. Code reads like blocking but doesn't block; backpressure handled differently. Reactive Java is in retreat.

- **Rust / Go async** — these languages didn't really adopt reactive patterns; channels + goroutines / async + tokio are simpler.

The high-water mark of reactive programming was around 2018-2020. Since then, language-level async (async/await, virtual threads) has handled what reactive promised, more simply.

Concrete example: search-as-you-type

The canonical reactive use case:

```typescript

const searchResults$ = searchInput$.pipe(

debounceTime(300),

distinctUntilChanged(),

filter(query => query.length >= 2),

switchMap(query => searchApi(query).pipe(

catchError(err => of([]))

))

);

searchResults$.subscribe(results => updateUI(results));

```

Without reactive:

```typescript

let lastQuery = '';

let cancelToken = null;

const onInput = debounce(async (query) => {

if (query === lastQuery || query.length < 2) return;

lastQuery = query;

if (cancelToken) cancelToken.abort();

cancelToken = new AbortController();

try {

const results = await searchApi(query, {signal: cancelToken.signal});

updateUI(results);

} catch (err) {

if (!cancelToken.signal.aborted) updateUI([]);

}

}, 300);

```

The reactive version is shorter and more obviously correct. For these specific patterns, reactive earns its keep.

Tools

- **RxJS** — JavaScript/TypeScript. The dominant reactive lib.

- **Project Reactor** — Java. Used in Spring WebFlux.

- **Akka Streams** — Scala/Java. Stream-processing-oriented; powerful, complex.

- **ReactiveX** family — bindings in many languages (RxSwift, RxKotlin, RxRust).

- **MutationObserver, ResizeObserver** — DOM equivalents; reactive in spirit.

A pragmatic position

Use reactive for streams. Use async/await for sequential async. Don't conflate them.

Specifically:

- **UI event streams** — reactive wins.

- **Data pipelines with backpressure** — reactive wins.

- **REST handlers, simple async** — async/await wins.

- **Mixed code** — keep them separate; bridge at boundaries.

The mistake is the all-or-nothing stance. Reactive is a tool; use where it fits.

Further reading

- [ConcurrencyPatterns]() — concurrency primitives broadly

- [EventDrivenArchitecture]() — events at the architecture level

- [ReactBestPractices]() — React's specific take on reactivity