Intent
Define a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (the observers) are notified and updated automatically — without the subject knowing their concrete types.
That single sentence is worth memorizing verbatim. In an interview, lead with it before you draw anything: it signals you know why the pattern exists, not just its shape.
The Problem
You have an object whose state matters to several others. The naive approach hard-wires the dependents into it:
// ANTI-PATTERN: the subject knows every consumer concretely
class Order {
private status: string = "PENDING";
constructor(
private emailService: EmailService,
private inventoryService: InventoryService,
private analytics: AnalyticsClient,
) {}
ship(): void {
this.status = "SHIPPED";
// Every new consumer = edit this method + the constructor.
this.emailService.sendShippedEmail(this);
this.inventoryService.decrement(this);
this.analytics.track("order_shipped", this);
}
}Three things are wrong here, and you should be able to name them:
- Open/Closed violation. Adding a fraud-check listener means editing
Order— a class that has nothing to do with fraud. - Tight coupling.
Orderdepends on the concreteEmailService,InventoryService, etc. It cannot be unit-tested without all of them. - Wrong direction of control. The thing that changed shouldn’t be responsible for orchestrating everyone who cares. That’s a single-responsibility smell.
Observer inverts this: consumers register interest with the subject, and the subject broadcasts to an abstract list it knows nothing about.
Structure
+------------------------+ +----------------------+
| <<interface>> | observers | <<interface>> |
| Subject |<>----------->| Observer |
+------------------------+ * +----------------------+
| + attach(o: Observer) | | + update(s: Subject) |
| + detach(o: Observer) | +----------------------+
| + notify() | ^
+------------------------+ |
^ +-----------+-----------+
| | |
+----------------------+ +----------------------+ +----------------------+
| ConcreteSubject | | EmailObserver | | InventoryObserver |
+----------------------+ +----------------------+ +----------------------+
| - state | | + update(s) | | + update(s) |
| - observers: List | +----------------------+ +----------------------+
| + getState() |
| + setState() |--- on change, calls notify() which loops
+----------------------+ observers and calls update() on each
The diamond (<>) is aggregation: the subject holds observers but does not own their lifecycle. That detail is the seed of the memory-leak pitfall we hit later — and, as you’ll see, the C++ implementation has to make an explicit choice (strong vs weak references) to honor it.
Participants
| Participant | Responsibility |
|---|---|
| Subject (interface) | Maintains the observer list; exposes attach/detach/notify. Knows observers only through the abstract Observer type. |
| Observer (interface) | Defines the update() callback the subject invokes. |
| ConcreteSubject | Holds the real state; calls notify() after any state change worth broadcasting. |
| ConcreteObserver | Implements update(); reacts to the change (and, in pull, queries the subject for what it needs). |
Push vs Pull — the design decision interviewers probe
When the subject notifies, how much does it hand over?
- Push model: the subject sends the changed data as arguments —
update(orderId, "SHIPPED"). Observers get exactly what they need with no extra call, but the signature must satisfy every observer, so it tends to over-deliver or bloat. - Pull model: the subject sends only a reference to itself —
update(this)— and each observer pulls the bits it cares about. Maximally decoupled from the data shape, but observers must know the subject’s interface and may make redundant queries.
Rule of thumb: push a small, stable event payload (an immutable value object) and let that be the contract. This gets you push-like ergonomics without coupling observers to the subject’s internals — the sweet spot most production event systems land on.
Push vs pull is not “which is correct” — it’s a coupling-vs-convenience trade. Push couples observers to the payload shape; pull couples them to the subject’s interface. State the trade-off out loud and pick based on how many observer types you expect and how volatile the data is.
A concrete example: an order-event bus
Forget the textbook weather-station. Here’s a realistic slice: an Order aggregate that emits typed events, with observers for notifications, inventory, and analytics. We push an immutable event (best-practice middle ground) and we make unsubscription explicit. Note all three languages stamp the event with a real wall-clock time in epoch milliseconds — TS Date.now(), C++ duration_cast<milliseconds>(...), and Python time.time_ns() // 1_000_000 (since bare time.time() returns fractional seconds, not millis) — so the at payloads are numerically equivalent across the three.
type OrderEvent = {
readonly orderId: string;
readonly status: "PLACED" | "SHIPPED" | "CANCELLED";
readonly at: number;
};
interface OrderObserver {
onOrderChanged(e: OrderEvent): void;
}
class Order {
private observers = new Set<OrderObserver>();
private status: OrderEvent["status"] = "PLACED";
constructor(public readonly orderId: string) {}
subscribe(o: OrderObserver): () => void {
this.observers.add(o);
return () => this.observers.delete(o); // unsubscribe handle
}
ship(): void {
if (this.status !== "PLACED") throw new Error("illegal transition");
this.status = "SHIPPED";
this.notify();
}
private notify(): void {
const e: OrderEvent = { orderId: this.orderId, status: this.status, at: Date.now() };
// Snapshot so an observer unsubscribing mid-loop can't corrupt iteration.
for (const o of [...this.observers]) {
try { o.onOrderChanged(e); }
catch (err) { console.error("observer failed", err); } // isolate failures
}
}
}
class EmailObserver implements OrderObserver {
onOrderChanged(e: OrderEvent): void {
if (e.status === "SHIPPED") console.log(`Email: order ${e.orderId} shipped`);
}
}Three production-grade details in that code that score points in an interview:
- Immutable event payload — observers can’t mutate shared state or each other. In TS this is
readonlyfields, in Python afrozendataclass, and in C++ the members are plain values (so the event stays copyable/movable for queues and relaying) with immutability enforced by passingconst OrderEvent&. - Snapshot-before-iterate — an observer that unsubscribes (or subscribes) inside
update()won’t corrupt the loop or throw a concurrent-modification error. - Failure isolation — one throwing observer doesn’t abort delivery to the rest. (Mention this; most candidates forget it.)
The C++ here deliberately stores std::weak_ptr, so it demonstrates the non-owning strategy the prose recommends rather than just naming it. The lock() + prune loop in notify() is the canonical weak-reference pruning pattern — exactly what a senior reviewer expects to see. (Note: a std::unordered_set<shared_ptr> would hash by pointer identity, which is fine; we use a vector<weak_ptr> here because weak_ptr isn’t directly hashable and we want the prune-on-notify sweep. Similarly, the Python set relies on observers’ default identity hashing — safe unless you give observers an eq-generating dataclass, which could collide.)
The memory-leak pitfall — the #1 thing seniors check for
Observer’s dirty secret: a live subject holding a strong reference to an observer keeps that observer alive. If observers register but never detach, they leak — and they keep receiving events for state they no longer care about. This is the classic “lapsed listener” leak that plagues long-lived subjects (a global event bus, a cache, a UI store).
Forgetting to unsubscribe is the most common Observer bug in real systems. A view subscribes to a store, the view is destroyed, but the store still holds it — the GC can never collect it, and worse, its update() fires on a dead view. Always return an unsubscribe handle at subscribe() time and call it in the consumer’s teardown (ngOnDestroy, useEffect cleanup, dispose(), destructor).
Strategies, in rough order of preference:
- Explicit unsubscribe handle (shown in the TS/Python above):
subscribe()returns a closure that detaches. Hard to forget when teardown is co-located with setup. - Weak references to observers (C++
std::weak_ptr— shown in the C++ above —, JavaWeakReference, JSWeakRef/WeakMap). The subject doesn’t keep observers alive; dead ones are pruned (see thelock()/erasesweep innotify()). Use when you genuinely can’t control observer lifecycle — but weak refs make delivery non-deterministic (an observer can vanish between two notifications with no warning), so prefer explicit unsubscription when you can. - Scoped/auto-disposing subscriptions — RAII in C++,
using/Disposablepatterns, framework lifecycle hooks.
Note on the C++ trade-off: storing
std::shared_ptrinstead would make the subject a co-owner and re-introduce the exact leak above unlessunsubscribe()is reliably called. That’s why the sample usesweak_ptr— it makes the “subject does not own observers” claim true in code, not just in a comment.
In a multithreaded subject, notify() iterating the observer list while another thread attach/detaches is a data race. Guard the list with a mutex, and — critically — snapshot the list under the lock, then release it before invoking callbacks. Holding the lock across update() invites deadlock if an observer re-enters the subject. One subtle, interview-grade point: the snapshot of strong references (or the live vector of locked shared_ptrs in the C++ above) also keeps those observers alive for the duration of the callback even if another thread unsubscribes mid-delivery — so an already-snapshotted observer still gets this event. That ties the threading and lifecycle stories together: snapshotting buys both iteration safety and a well-defined “you’ll receive the event you were enrolled for” guarantee.
When to use it
- Event systems / domain events: “when X happens, an open-ended set of things should react.” Order placed, user signed up, file changed.
- MVC / MVVM: the Model is the subject; Views observe it. Change the model once, every view refreshes. This is the pattern’s historical home (Smalltalk-80).
- Reactive UIs and state stores: Redux/Zustand/RxJS
Subject, Vue reactivity, signals — all are Observer at the core (a value holder notifying dependents on change). - Cache invalidation, configuration hot-reload, progress reporting.
When NOT to use it (over-use & pitfalls)
- Cascade / “notification storms.” Observer A’s
update()mutates B, which notifies C, which mutates A… You get re-entrancy, infinite loops, or O(n²) update floods. If you have a web of mutual dependencies rather than a clean one-to-many, reach for Mediator instead. - Order-dependent observers. If observer B only works correctly after observer A has run, you’ve smuggled hidden coupling through an unordered list. Observer guarantees no ordering; if you need a pipeline, model it as one explicitly.
- You need the result back. Observer is fire-and-forget broadcast. If the subject needs return values or must coordinate responses, it’s the wrong tool.
- Debuggability cost. Control flow becomes implicit — “who fired this update?” is hard to trace. For a single known consumer, a plain method call is clearer than a subscription.
Relationship to SOLID
- Open/Closed: the headline win. New reactions = new observers, zero edits to the subject. This is the canonical OCP demonstration.
- Single Responsibility: the subject’s job is to manage state and announce changes — not to know what shipping an order means to email, inventory, or analytics.
- Dependency Inversion: the subject depends on the abstract
Observer, not concretions. The arrows point at the interface.
Sibling patterns it’s confused with
| Pattern | How it differs from Observer |
|---|---|
| Pub/Sub (message bus) | Adds a broker between publisher and subscriber. In Observer the subject holds direct references to observers; in Pub/Sub they’re fully decoupled through a channel/topic and often async/cross-process. Observer is in-process and synchronous by default. |
| Mediator | Centralizes many-to-many communication in one hub to eliminate a web of references. Observer is one-to-many broadcast. Use Mediator when objects must coordinate; Observer when one announces and others independently react. |
| Reactive streams (Rx) | A superset — Observer plus composition operators (map/filter/merge), backpressure, completion, and error channels. RxJS Observable is “Observer done industrially.” |
| Chain of Responsibility | Passes a request along until one handler consumes it. Observer delivers to all observers; CoR stops at the first that handles it. |
In an interview, when you say “Observer,” immediately bound it: “in-process, synchronous, subject holds direct refs; if they wanted cross-service or async I’d reach for Pub/Sub, and if it’s many-to-many I’d reach for Mediator.” That one sentence shows you know the neighborhood, not just the single pattern — which is exactly the level of fluency that separates an SSE answer from a junior one.