Hermes Wiki

Observer Pattern

Concept

Observer defines a one-to-many dependency between a "subject" and a set of "observers" so that when the subject's state changes, all registered observers are notified automatically, without the subject needing to know anything about who its observers are or what they do with the notification. The subject exposes a narrow interface — typically subscribe/attach, unsubscribe/detach, and an internal notify it calls after any state change — and observers implement a matching update callback. The subject holds a list of observer references and iterates it on notify; it never imports or depends on any concrete observer type, only the observer interface.

This decoupling is the entire point: the subject can gain, lose, or swap observers at runtime with zero code changes on either side, and new observer types can be added without touching the subject at all. It is one of the most widely-implemented GoF patterns in practice, usually under a different name — DOM addEventListener, Node's EventEmitter, RxJS/reactive-streams Observable.subscribe, pub/sub message brokers, and framework reactivity systems (Vue's reactivity, MobX, Svelte's stores) are all Observer at their core, differing mainly in whether notification is synchronous/in-process (classic Observer) or asynchronous/out-of-process (pub/sub over a broker). React's rendering model is a related but distinct case: state changes trigger re-render via a diffing/scheduling layer rather than direct observer callbacks, though early Flux implementations were closer to classic Observer.

Tradeoffs

Approach Benefit Cost
Observer (subject holds direct references, calls observers synchronously) Simple, in-process, no infrastructure; notification order is deterministic and callable stack traces are readable Subject and observers share a process/lifetime — a slow or throwing observer blocks the subject and other observers; doesn't scale across process/service boundaries
Pub/Sub over a message broker (subject publishes an event, broker fans out to subscribers) Publisher and subscribers fully decoupled across processes/services; broker handles delivery, retry, backpressure Added infra dependency and operational surface (broker uptime, message ordering/dedup); notification is async, so debugging a chain of effects is harder
Polling (observers periodically ask the subject "did you change?") No subject-side notification logic needed at all; trivially simple to reason about Wastes work on unchanged state, and introduces a detection-latency window bounded by the poll interval — the opposite of Observer's near-immediate push

Observer and pub/sub are the same pattern at different scales: pub/sub is what Observer becomes once the subject and observers can no longer share memory and a network hop (with its own failure modes — a subscriber that's down, message loss, ordering) gets inserted between "state changed" and "observer notified."

When to use / when not to

  • Use when multiple parts of a system need to react to another part's state changes, and those parts shouldn't need compile-time knowledge of each other — UI components reacting to model changes, cache invalidation listeners, audit/logging hooks that shouldn't live inside the business logic they're observing.
  • Especially valuable when the set of observers is expected to change at runtime or grow over the system's life; a subject built with Observer from the start absorbs new observer types with zero subject-side changes.
  • Don't reach for it when there's exactly one fixed consumer of a state change known at compile time — a direct method call is simpler, more debuggable, and avoids the indirection cost for no decoupling benefit.
  • Avoid a synchronous, in-process Observer specifically when observers can be slow, unreliable, or need to run in a different process — that's the pub/sub tradeoff table's job, not classic Observer's.

Common pitfall

Forgetting to unsubscribe when an observer's lifetime ends shorter than the subject's — the subject holds a reference to a now-dead or now-irrelevant observer, which either leaks memory (the observer object can never be garbage collected while the subject holds it) or, worse, fires a callback into an observer that assumes it's still attached to live state (a React component updating state after unmount is exactly this failure mode). The fix is symmetric lifecycle discipline: every subscribe needs a matching unsubscribe tied to the observer's own teardown path, not left to be cleaned up "eventually" by the subject.

Engineering Lens

Observer is the pattern that makes "who depends on this state changing" an explicit, inspectable relationship instead of an implicit one buried in call order. In a design review, the sharp question isn't "did you use an event emitter" — it's "when this list of subscribers grows to include a slow or failing one, what happens to the others," because the answer reveals whether the design accounted for exactly the coupling-through-notification failure mode the pattern is meant to avoid (an observer that throws or blocks shouldn't be able to take down its siblings or the subject itself). That's also why production Observer implementations almost always wrap notify in per-observer error isolation, even though the textbook version of the pattern doesn't mention it.

  • Mediator Pattern — both decouple communicating components, but Mediator centralizes interaction logic in a hub while Observer keeps the subject unaware of its observers entirely
  • Publish-Subscribe Messaging — Observer generalized across process boundaries via a broker

Sources

Hermes Wiki