Reactor Pattern with core.async for Scalable Event-Driven Applications

Learn how to model the Reactor pattern in Clojure with channels and dispatch loops, and how to keep the reactor non-blocking without confusing it with general-purpose background work.

Reactor pattern: A design in which a central dispatcher waits for events or readiness signals, routes them quickly, and keeps the hot path responsive by pushing slow work outward.

The Reactor pattern is easy to describe loosely and implement badly. It is not “any async code.” It is a design centered on fast dispatch. In Clojure, core.async can model the handoff points clearly, but only if the reactor loop stays narrow.

What the Reactor Should Own

A reactor should own:

  • event ingress
  • event classification
  • dispatch to the right handler or downstream stage

It should not own:

  • long blocking I/O
  • expensive data transformation
  • unrelated business workflows

That distinction is the difference between a reactor and a bottleneck.

A Small Reactor Loop

1(require '[clojure.core.async :as async])
2
3(defn start-reactor [in handlers]
4  (async/go-loop []
5    (when-some [event (async/<! in)]
6      (when-let [handle! (get handlers (:type event))]
7        (handle! event))
8      (recur))))

This loop is intentionally boring. That is a strength. It makes the real design questions visible:

  • how are events classified?
  • what happens when input is too fast?
  • where does slow work go?

Reactor vs Worker Pool

These are not the same thing:

  • the reactor dispatches quickly
  • a worker pool performs slower downstream work

If the dispatch loop starts behaving like a worker pool, the reactor has lost its job.

Backpressure and Overload

Every reactor needs an overload strategy:

  • bounded input buffer
  • dropping or sampling low-value events
  • fast rejection
  • handoff to retry or dead-letter paths

Without an explicit overload answer, the reactor becomes a place where latency quietly accumulates.

Handler Contracts Matter

Reactor handlers should have clear rules:

  • what part of the event they may inspect
  • whether they may enqueue more work
  • whether they are allowed to block
  • where failures are reported

If handlers are unconstrained, the reactor loop stays small in code but becomes operationally unpredictable because any handler can quietly violate the design.

Event Shape Is Part of Reactor Design

Reactors work best when incoming events already have a stable shape. A handler should not need to guess whether an event is complete, whether fields are optional, or whether several unrelated event kinds were packed into one ambiguous payload.

Useful reactor events usually make these things explicit:

  • event type
  • routing key or source
  • enough context to hand work off safely
  • whether the event is best-effort, retryable, or terminal

Reactor vs Event Loop

An event loop is the control mechanism. A reactor is the design pattern built around quick readiness-driven dispatch. Many reactors contain an event loop, but the reactor pattern is more specific about responsiveness and offloading.

Observe the Hot Path

Useful reactor metrics usually include:

  • input rate
  • dispatch latency
  • queue depth
  • dropped event count
  • downstream handoff failures

These metrics show whether the reactor is still acting like a fast dispatcher or has started absorbing too much work.

Practical Rule

Model a reactor in Clojure as a very small dispatch center with explicit handoff to workers or stages. The moment the central loop starts doing heavy work itself, the design has drifted away from the pattern.

Revised on Thursday, April 23, 2026