Observer Pattern Using core.async Channels

Learn how the Observer pattern maps to `core.async` channels, pub/sub, and fan-out workflows in Clojure, and where channel-based observation is stronger or weaker than direct callback relationships.

Observer pattern: A design where one component publishes changes and multiple interested components react to those changes without being tightly coupled to the publisher.

In Clojure, Observer is often better modeled as event distribution than as an object maintaining a list of callback subscribers. core.async channels, mult, and pub provide cleaner coordination tools for that style, especially when updates should flow through explicit asynchronous boundaries.

From Callback Lists to Event Streams

A traditional Observer implementation often looks like:

  • one subject
  • many observers
  • direct notification on each change

In Clojure, the subject can instead publish events onto a channel, and downstream consumers decide how to process them. That shifts the model from “who do I call?” to “what event happened?”

A Simple Fan-Out Example

 1(require '[clojure.core.async :as a])
 2
 3(def events (a/chan 10))
 4(def broadcast (a/mult events))
 5
 6(def audit-ch (a/chan 10))
 7(def metrics-ch (a/chan 10))
 8
 9(a/tap broadcast audit-ch)
10(a/tap broadcast metrics-ch)
11
12(a/>!! events {:type :order-created :order-id 42})

The producer does not know about the audit or metrics consumers directly. It emits an event. The fan-out structure handles the observation relationship.

When pub Is Better Than mult

Use mult when all subscribers should see every event. Use pub when observers depend on a topic:

1(def router (a/pub events :type))
2
3(def orders-ch (a/chan 10))
4(def invoices-ch (a/chan 10))
5
6(a/sub router :order-created orders-ch)
7(a/sub router :invoice-issued invoices-ch)

That is often a better model for event-heavy systems than one giant callback registry because the routing rule is explicit and testable.

The Visual Difference

The visual below shows the shift from direct callback coupling toward event fan-out.

    flowchart LR
	    A["Producer"] --> B["Event channel"]
	    B --> C["Fan-out or topic routing"]
	    C --> D["Observer 1"]
	    C --> E["Observer 2"]
	    C --> F["Observer 3"]

The important point is that observers now depend on the event contract and the channel topology, not on the publisher’s internal subscriber list.

Where This Pattern Fits Well

Channel-based Observer is strong when:

  • multiple consumers should react independently
  • producers should not know about all subscribers
  • backpressure and buffering matter
  • observation needs topic routing or async decoupling

It is weaker when:

  • the updates are strictly synchronous
  • ordering guarantees are simple and local
  • a direct function call would be clearer

If one publisher and one consumer live in the same small function boundary, channels may be unnecessary ceremony.

Common Failure Modes

This pattern goes bad when:

  • channel topologies become harder to understand than the business flow
  • consumers silently block and stall the system
  • buffering assumptions are never documented
  • events are too vague to support stable subscribers

Observer via channels still needs contracts. Loose coupling does not mean no discipline.

Practical Heuristics

Use channel-based observation when events are a real domain concept and multiple consumers need decoupled reactions. Prefer direct function calls when the relationship is local, synchronous, and not expected to evolve.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026