skip to content

What problem does the Observer design pattern solve, and what are the responsibilities of the subject and the observer?

level: juniorimportance: must knowfreq 85%

answer

  1. one-to-many, auto-notify
  2. attach / detach / notify / update
  3. subject knows only the interface
  4. new listener = new class, not an edit
  5. costs: leak, order, cascade, hidden flow

basics

~10 s

Observer defines a one-to-many dependency: many observers register with one subject, and when the subject's state changes it notifies them all automatically. The subject knows only an abstract observer interface, not the concrete observers.

solid answer

~50 s

Observer answers "many things must react when one thing changes" without the changing thing knowing who those things are. The subject owns the state plus a registration list, and exposes attach(observer) / detach(observer) plus an internal notify() that calls update() on each registered observer. Each observer implements one small interface and decides what to do with the change. The payoff is dependency decoupling: the subject depends only on an abstraction, so a new reaction (a second chart, an audit log, a cache invalidation) is a new class rather than an edit to the subject — Open/Closed plus Dependency Inversion in practice. The costs are equally real: notification is implicit control flow that is hard to trace in a debugger, observers that never detach keep the subject holding them alive (the lapsed-listener leak), notification order is usually unspecified, and one state change can cascade into an update storm.

code

pseudocode · 19 lines
pseudocode
interface Observer { update(event) }

class PriceFeed {                 // Subject
    private observers = []
    private price

    subscribe(o)   { observers.add(o); return () => observers.remove(o) }

    setPrice(p) {
        if (p == price) return                 // suppress no-op notifications
        price = p
        for (o of observers.snapshot())         // snapshot: safe against detach-during-notify
            try { o.update(PriceChanged(p)) }
            catch (e) { log(e) }                // one bad observer must not kill the rest
    }
}

// wiring happens outside the subject
unsubscribe = feed.subscribe(chart)

go deeper

for a junior

Name the intent (one-to-many, automatic notification), the two roles, and the four methods (attach/detach/notify/update). Give one concrete example such as a UI button and its click handlers.

for a middle

Add why it exists: Open/Closed and Dependency Inversion, model not depending on views. Mention push vs pull notification and that you must unsubscribe.

for a senior

Lead with the trade-offs: hidden control flow, lapsed-listener leaks, undefined ordering, reentrancy and update storms, exception isolation, snapshot-during-iteration. Explain subscription handles/scoped lifetimes as the disciplined fix.

for a principal

Frame it as a coupling decision: in-process Observer buys extensibility at the cost of traceability and lifetime management, and it does not buy temporal decoupling — that requires a broker, with its own costs (eventual consistency, at-least-once delivery, idempotency). Set team conventions: subscription handles, no ordering dependence, no writes back into the subject from update().

## The problem it solves One piece of state changes — a price, a spreadsheet cell, a selected row, an entity being saved — and several unrelated parts of the system must react: redraw a chart, refresh a table, append an audit record, invalidate a cache. The naive version has the state-changing code call each reaction directly: ``` setPrice(p): this.price = p chart.redraw() table.refresh() auditLog.record(p) ``` This compiles and works, but the price holder now *depends on* the chart, the table and the audit log. Every new reaction edits this method; the price holder cannot be reused or unit-tested without dragging in UI and logging; and low-level code (the model) depends on high-level code (the views), which is backwards. ## The structure **Observer** (also called *Listener*, *Dependents*, or *Publish–Subscribe* in its in-process form) inverts that dependency by introducing an abstraction: - **Subject** (a.k.a. Observable, Publisher): holds the state that changes; holds a collection of registered observers; offers `attach(observer)` / `detach(observer)` (also spelled subscribe/unsubscribe, addListener/removeListener); and, whenever its state changes, calls its own `notify()` which loops over the collection calling `update()` on each. - **Observer** (a.k.a. Listener, Subscriber): a narrow interface with essentially one method, `update(...)`. Concrete observers implement it and do whatever they need. ``` interface Observer { update(event) } class Subject: observers = [] attach(o): observers.add(o) detach(o): observers.remove(o) notify(e): for o in snapshot(observers): o.update(e) setPrice(p): if p == price: return // don't notify on no-op price = p notify(PriceChanged(p)) ``` The crucial line is the direction of the arrows: `Subject → Observer` (an interface it owns or that lives in a shared abstraction), and `ConcreteObserver → Observer`. The subject has **zero** compile-time knowledge of charts, tables or audit logs. Registration is what wires them at runtime, usually in a composition root or in the observer's own initialization. ## Terms defined - **One-to-many dependency**: one subject, arbitrarily many dependents, each of which must stay consistent with the subject's state. - **Decoupling**: here it means *dependency* decoupling (the subject doesn't reference observer types), not *temporal* decoupling — the classic Observer notification is a plain synchronous method call on the same thread and the same call stack as the state change. - **Registration / subscription**: the runtime act of adding an observer to the subject's list. Its inverse, deregistration, is the part people forget. ## Where you have already seen it - GUI event handlers: a button is the subject, your click handler is the observer. - The Model–View pattern family: views observe the model so several views stay in sync with one model. - Spreadsheets: cells observe the cells they reference; changing one recalculates the dependents. - Language/runtime facilities: callback lists, `PropertyChangeListener`-style APIs, `EventEmitter.on(...)`, signals/slots, reactive streams (`subscribe`), DOM `addEventListener`, database triggers conceptually. In modern code the "observer" is very often just a function/lambda rather than a class implementing an interface — that is the same pattern with a lighter syntax, and it makes deregistration *harder* because you no longer naturally hold a reference to the thing you registered. ## Benefits 1. **Open/Closed**: new reactions are added without modifying the subject. 2. **Dependency Inversion**: the model stops depending on the presentation/infrastructure. 3. **Runtime reconfiguration**: observers come and go while the program runs. 4. **Broadcast semantics**: the subject doesn't care whether there are 0 or 50 listeners. ## Costs and failure modes (say these in an interview) 1. **Implicit control flow**: reading `setPrice` tells you nothing about what actually happens. Debugging means stepping through `notify()` to discover the listener set. 2. **Lapsed listener / leak**: the subject's list holds strong references. An observer that forgets to detach is never collected and keeps being called — often after it is logically dead. 3. **Unspecified ordering**: observers usually get called in registration order, which is an accident of wiring, not a contract. Depending on it produces fragile systems. 4. **Cascades and storms**: an observer's reaction may change other subjects, which notify their own observers; N changes × M observers can explode, and an observer that writes back to the subject can cause infinite recursion. 5. **Failure coupling**: with synchronous notification, an exception in observer #2 can prevent #3 from ever running and can propagate back into the caller that merely set a value. 6. **Concurrency**: if the list is mutated during iteration (an observer unsubscribing itself), you corrupt the loop unless you iterate a snapshot. ## Related patterns (useful contrast) - **Mediator**: many-to-many coordination routed through one hub that *does* know the participants; Observer is one-to-many with an anonymous many. - **Publish–Subscribe with a broker**: adds an intermediary and usually asynchrony/durability; publishers and subscribers do not even hold references to one another. - **Chain of Responsibility**: passes a request along until someone handles it (exactly one handler); Observer notifies everyone. - **Reactive streams / event sourcing**: Observer scaled up with backpressure, composition operators, and persistence.

  • Does Observer decouple the subject from its observers completely?
    No. It removes the compile-time dependency on concrete observer types, but classic Observer is still synchronous and same-thread: the subject blocks until every observer returns, exceptions can propagate back, and the subject holds strong references to the observers. Temporal and lifetime coupling remain; only a broker/queue removes those.
  • How is Observer different from Mediator?
    Mediator centralizes many-to-many interaction in a hub that knows the colleagues and encodes the coordination rules; colleagues talk only to the mediator. Observer is one subject broadcasting to an anonymous set of dependents with no coordination logic. Mediator reduces N-to-N wiring; Observer reduces knowledge of who is listening.
  • When would you not use Observer?
    When there is exactly one, always-present dependent (just call it), when the reaction must be part of the same transaction with guaranteed ordering and error handling (make it an explicit step), or when the indirection would hide critical business flow that reviewers need to read linearly.

A magazine subscription. The publisher keeps a mailing list, not a list of people it personally knows; it prints one issue and every subscriber gets it. New subscribers change nothing about how the magazine is produced — and a subscriber who moves without cancelling keeps generating mail forever, which is exactly the lapsed-listener leak.

saying these in an interview costs you the question

  • Saying Observer makes the subject and observers 'fully independent' — the subject still holds references and still blocks on synchronous callbacks.
  • Claiming Observer is inherently asynchronous or multi-threaded; classic Observer is a direct method call on the caller's thread.
  • Confusing Observer with Mediator or with a message broker/pub-sub middleware.
  • Assuming notification order is guaranteed and designing observers that depend on running first.
  • Forgetting detach entirely and treating memory leaks as a runtime problem rather than a pattern-level obligation.
  • Notifying on every setter call even when the value did not actually change.

context