CMD Guide
HomeOO & Low-Level DesignBehavioral

Observer Pattern

What problem does it solve?

The Observer pattern defines a one-to-many dependency between objects: when one object — the subject (also called the observable) — changes state, every object that depends on it — its observers — is notified and updated automatically through a common update() callback. The subject keeps a list of observers and, on each state change, walks that list and invokes update() on each one, so observers stay in sync without the subject ever knowing their concrete types. That is the whole point: loose coupling.

Picture a weather station that feeds several displays: a current-conditions panel, a statistics panel, and a forecast panel. Each must refresh whenever new measurements arrive. The naive design wires every display directly into the station, so adding a fourth display means editing the station's code. That tight coupling makes the system rigid and hard to extend. The Observer pattern breaks that coupling: the station publishes updates, and any display that has subscribed receives them — with the station blind to the concrete display types.

diagram
diagram

Structure

The pattern is built from four roles. Two define contracts (interfaces), two are the concrete classes that implement them.

  1. Subject (Observable) — an interface declaring registerObserver(o), removeObserver(o), and notifyObservers(). It owns the subscription list.
  2. Concrete Subject — holds the real state. When that state changes it calls notifyObservers(), walking its list and invoking each observer's update().
  3. Observer — an interface declaring a single update(...) method that the subject calls.
  4. Concrete Observer — implements update() to react to the change, often keeping a copy of the relevant state in sync with the subject.

The key property: the concrete subject depends only on the Observer interface, never on the concrete observer classes. That is what lets you add new observers without touching the subject.

diagram
diagram

Implementation walkthrough

The subject keeps a list of observers and three operations over it: register, remove, and notify. Whenever its measured state changes it calls notifyObservers(), which iterates the list and pushes the new values into each observer's update(). Each concrete display registers itself with the subject in its constructor, so subscription is automatic at construction time.

interface Observer {
  void update(float temperature, float humidity, float pressure);
}

interface Subject {
  void registerObserver(Observer o);
  void removeObserver(Observer o);
  void notifyObservers();
}

class WeatherStation implements Subject {
  private List<Observer> observers = new ArrayList<>();
  private float temperature, humidity, pressure;

  public void registerObserver(Observer o) { observers.add(o); }
  public void removeObserver(Observer o)   { observers.remove(o); }

  public void notifyObservers() {
    for (Observer o : observers)
      o.update(temperature, humidity, pressure);
  }

  public void setMeasurements(float t, float h, float p) {
    this.temperature = t; this.humidity = h; this.pressure = p;
    notifyObservers();          // state changed -> fan out
  }
}

class CurrentConditionsDisplay implements Observer {
  CurrentConditionsDisplay(Subject s) { s.registerObserver(this); }
  public void update(float t, float h, float p) {
    System.out.println("Current conditions: " + t + "F, " + h + "% humidity");
  }
}

The same shape in Go uses a slice of an Observer interface instead of a List:

type Observer interface {
    Update(temp, humidity, pressure float64)
}

type WeatherStation struct {
    observers                     []Observer
    temperature, humidity, pressure float64
}

func (w *WeatherStation) Register(o Observer) { w.observers = append(w.observers, o) }

func (w *WeatherStation) Notify() {
    for _, o := range w.observers {
        o.Update(w.temperature, w.humidity, w.pressure)
    }
}

func (w *WeatherStation) SetMeasurements(t, h, p float64) {
    w.temperature, w.humidity, w.pressure = t, h, p
    w.Notify()
}

Tracing one state change

Say all three displays have registered, in this order: [current, statistics, forecast]. A sensor reading arrives and the app calls station.setMeasurements(80f, 65f, 30.4f). Follow the control flow:

#WhereWhat happensVisible effect
1setMeasurementsfields written: temperature=80, humidity=65, pressure=30.4subject state updated
2setMeasurementscalls notifyObservers()fan-out begins
3loop, observers[0]current.update(80, 65, 30.4)prints Current conditions: 80.0F, 65.0% humidity
4loop, observers[1]statistics.update(80, 65, 30.4)records the reading, prints running avg/min/max
5loop, observers[2]forecast.update(80, 65, 30.4)recomputes and prints the forecast
6notifyObserversloop ends, method returnscontrol returns to the caller

The subject issued one call. Three independent reactors ran, in registration order, and the subject never referenced any of their concrete classes. Add a fourth display tomorrow and steps 1–2 and 6 are byte-for-byte identical — only the loop visits one more entry.

Push vs. pull — and the closest alternative

There are two ways the subject can deliver a change, and the choice is the main design tension inside the pattern.

The closest structural alternative is a plain event bus / publish-subscribe broker. Compared to Observer, a pub-sub bus inserts a third party between publisher and subscriber, so the two never hold references to each other at all — better for cross-process or cross-service decoupling, but you lose the direct, synchronous, in-process update guarantee that Observer gives you and you take on broker infrastructure. Observer is the right tool when subject and observers live in the same process and you want a simple, synchronous, in-memory notification with no broker.

Pitfalls — the ones interviewers probe

1. Concurrent modification during notify (the #1 gotcha)

A very common requirement: an observer unsubscribes itself the moment it receives a terminal event. It calls removeObserver(this) from inside its own update(). But update() was invoked by the subject while the subject is still iterating the observer list — so the observer is mutating the very collection being walked.

// A one-shot alert that unsubscribes itself after it fires.
class HeatAlert implements Observer {
  private final Subject station;
  HeatAlert(Subject s) { this.station = s; s.registerObserver(this); }

  public void update(float t, float h, float p) {
    if (t > 100) {
      System.out.println("Heat alert!");
      station.removeObserver(this);   // BUG: mutates the list mid-iteration
    }
  }
}

With the naive for (Observer o : observers) loop, this throws ConcurrentModificationException. Why: the enhanced-for uses ArrayList's iterator, which snapshots a modCount when it is created and compares it against the list's live modCount on every next(). The self-removal bumps the live modCount; the next next() sees the mismatch and fails fast. (This is fail-fast detection, "best effort" — it is not guaranteed to fire, so a silently skipped observer is the even nastier version of the same bug.) The same thing happens if an observer registers a new observer during update().

diagram
diagram

The three standard fixes — and their trade-offs

(a) Iterate over a snapshot (copy) inside notify. Copy the list before walking it, so the iterator you traverse is disconnected from the list that update() mutates:

public void notifyObservers() {
  for (Observer o : new ArrayList<>(observers))   // walk a copy
    o.update(temperature, humidity, pressure);
}

Simplest and correct for self-removal on the notifying thread. Cost: an O(n) allocation-and-copy on every notification. Also note the copy itself iterates observers, so it does not make you thread-safe against another thread mutating during the copy — that still needs a lock. Semantics: the observer that removed itself still receives this round's notification (it was already in the snapshot).

(b) Use CopyOnWriteArrayList as the backing store.

private final List<Observer> observers = new CopyOnWriteArrayList<>();
// notifyObservers() body is unchanged — the iterator walks an immutable snapshot

Its iterator traverses an immutable snapshot of the array taken when the iterator was created, so a registration or removal during notify never throws and is simply invisible to the in-flight pass. Reads/iteration are lock-free; each write (register/remove) copies the whole backing array — O(n) per write. That trade — cheap reads, expensive writes — is exactly right for a listener list, which is notified constantly but subscribed to rarely. It is also inherently thread-safe. This is the standard production choice.

(c) Defer mutations until the loop finishes. Set a "notifying" flag; while it is set, register/remove enqueue their intent instead of touching the live list, and you drain the queue after the loop:

private boolean notifying = false;
private final List<Observer> pending = new ArrayList<>();

public void removeObserver(Observer o) {
  if (notifying) pending.add(o);       // defer — don't touch the live list
  else observers.remove(o);
}

public void notifyObservers() {
  notifying = true;
  try {
    for (Observer o : observers)
      o.update(temperature, humidity, pressure);
  } finally {
    notifying = false;
    observers.removeAll(pending);       // apply deferred removals now
    pending.clear();
  }
}

(registerObserver mirrors this with its own pending list.) No per-notify copy and no per-write array copy — the cheapest option when both notifications and subscription churn are hot and n is large. Cost: more code, and, like the other two, the mutation is invisible until the current notify completes. Not thread-safe on its own — cross-thread use still needs a lock.

2. Thread-safety of the list itself

Even without self-removal, calling registerObserver/removeObserver from one thread while another thread runs notifyObservers() races on a plain ArrayList — you get torn reads, lost updates, or a ConcurrentModificationException. Guard all three methods with the same lock, or use CopyOnWriteArrayList, which makes each operation atomic and each iteration snapshot-consistent with no lock on the read path. (Separately: if update() touches shared state, that state needs its own synchronization — a thread-safe observer list does not make the observers' work thread-safe.)

3. Re-entrant / cyclic notification

If A observes B and B observes A, a change to A notifies B, whose update() changes B, which notifies A, which notifies B… — unbounded recursion ending in StackOverflowError. Break the cycle with a "already notifying" guard that ignores re-entrant notifications, or coalesce changes so a burst produces one notification.

4. Undefined ordering & mid-notify visibility

A List-backed subject happens to fire in registration order, but the pattern guarantees no order — observers must never assume they run before or after another observer. And a subscription made mid-notify may or may not be seen this pass (with a snapshot or COW list it will not be), so observers must not rely on seeing their own or others' mid-notify changes.

5. Lapsed-listener memory leak

The subject holds strong references to its observers, so an observer that forgets to unregister can never be garbage-collected (the subject keeps it alive) and keeps receiving updates it no longer wants. Fix with an explicit removeObserver in the observer's lifecycle/close hook, or hold observers via WeakReference so the GC can reclaim one nothing else references — at the cost of having to prune expired references and losing the guarantee that the subject keeps the observer alive.

6. Update storms & heavy work in update()

notifyObservers() runs every observer's update() synchronously on the subject's thread, in sequence: one slow or blocking observer stalls the whole fan-out and the subject. And a change whose update() mutates another subject can cascade into an update storm. Keep update() fast; offload heavy or blocking work to a queue/executor, and coalesce or batch high-frequency updates rather than fanning out on every mutation.

Applications, pros, and cons

Observer shows up wherever one state change must fan out to many reactors: GUI event handling (clicks, keypresses), data monitoring (stock tickers, dashboards), the model-to-view link in MVC, and notification services in social and news apps.

StrengthsCosts
Loose coupling — the subject knows only the Observer interface.Notification overhead — many observers or frequent changes can be expensive.
Open/closed — add observers without editing the subject.Ordering & mid-notify hazards — undefined order, and mutation during notify can throw or be missed.
Dynamic subscription — register and remove at runtime.Memory leaks — forgetting to deregister keeps observers alive (the lapsed-listener problem).
Broadcast communication — one change reaches many reactors.Harder debugging — control flow is indirect and hard to trace.

When to use it — and when not to

Reach for Observer when

Prefer something else when

Rule of thumb: Observer is the lightweight, in-process, one-to-many notifier. The moment you need cross-boundary decoupling, durability, complex coordination, or stream operators with backpressure, graduate to a bus, a mediator, or reactive streams (Flow) respectively — and for a single fixed listener, just call the method.

Takeaways

Putting a number on the abstraction tax

Every pattern decision above was argued qualitatively — "cheap reads, expensive writes", "an O(n) copy per write". That is how pattern choices are usually made, and it is how they go wrong: every abstraction costs indirection, but you cannot manage a cost you never measure. So attach an actual number. The discipline is the same for any pattern: write the per-operation cost as a formula, plug in your real n and your real read/write ratio, and let the arithmetic — not the aesthetic — pick the structure.

The COW crossover, in numbers

Recall the "standard production choice": back the observer list with CopyOnWriteArrayList. Its cost model is exact — each mutating call (register/remove) allocates a fresh array and copies all n current elements, so one subscribe or unsubscribe = one O(n) allocation + copy; iteration/notify is a lock-free read of an existing snapshot, ~O(1) to start. So the total copy work COW imposes is writes × n element-copies. Put that against the alternatives:

Listener set nWrite patternCOW copy work / secVerdict
100rare (1 sub/sec)~100 copies/secTrivial — COW is right. Notify pays nothing.
10,000rare (1 sub/sec)~10k copies/secStill fine — one 10k-element array copy per second is noise.
10,000churning (1,000 sub+unsub/sec)~10,000,000 copies/secCatastrophic — 10M element-copies + 1,000 array allocations/sec, all GC pressure, to notify a set that barely changes shape.

The crossover is not about n alone and not about write-rate alone — it is the product writes × n. COW is engineered for the top rows (read/iterate-heavy, write-rare); the bottom row is exactly the workload it is wrong for.

The named trade-off, priced

Three structures, same decision, now with cost attached rather than adjectives:

StructurePer-notify costPer-write costBest when
synchronized list + snapshot-on-notifyO(n) copy every notifyO(1) mutate (under lock)writes hot, notifies rare
CopyOnWriteArrayList~O(1) to start iterating (no copy)O(n) copy every writenotifies hot, writes rare
concurrent structure (e.g. ConcurrentHashMap newKeySet)O(n) weakly-consistent walk, no copy~O(1) amortizedboth hot, and you can tolerate a weakly-consistent iteration

Now the choice is arithmetic: notify-per-sec × copy-cost vs. write-per-sec × copy-cost. Snapshot-list moves the O(n) tax onto notify; COW moves it onto writes; the concurrent set refuses to pay a bulk copy on either path, spending instead a small per-entry overhead and giving up snapshot-consistent iteration (a mutation mid-walk may or may not be seen — acceptable for a listener list, as the pitfalls section already established).

Two more, so the habit sticks

The lesson is not these three numbers; it is the move: convert "this abstraction costs indirection" into a cost expressed in your real n and access ratio, then compare it to the named alternative's cost the same way. Measure the tax before you agree to pay it.


Adapted and expanded from the Knowledge Guide lesson "Observer Pattern" (OO & Low-Level Design › Behavioral), carrying forward its weather-station example and structure/implementation breakdown; the concurrent-modification analysis and fixes synthesized from the JDK ArrayList/CopyOnWriteArrayList fail-fast semantics and Java Concurrency in Practice (Goetz), push-vs-pull and alternatives from the Gang of Four Design Patterns, and the reactive comparison from the java.util.concurrent.Flow / Reactive Streams specification. Re-authored/Deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on Observer Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Observer Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Observer Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Observer Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Observer Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Observer Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes