Observer Pattern

Publish-subscribe for objects. Push vs pull, the leaky-listener problem, and modern event-bus alternatives.

Pattern Foundational
7 min read
pattern behavioral observer pub-sub events

What it is#

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically. The Gang of Four named the two roles Subject (the thing being watched) and Observer (the thing watching).

The pattern is what lets a stock-price ticker, a weather station, or a chat room notify many interested listeners without knowing who they are. The subject owns a list of observers, exposes attach and detach, and calls a notify method that walks the list and invokes a callback on each observer. The crucial property: the subject does not know — and does not need to know — the concrete types of its observers. They satisfy a small interface; that is all the subject is allowed to depend on.

Class structure#

┌──────────────────┐ ┌─────────────────────┐
│ Subject │◇───────►│ Observer │
├──────────────────┤ * ├─────────────────────┤
│ attach(Observer) │ │ update(event) │
│ detach(Observer) │ └─────────────────────┘
│ notify() │ ▲
└────────▲─────────┘ │
│ │
┌────────┴──────────┐ ┌────────────┴─────────────┐
│ ConcreteSubject │ │ ConcreteObserver │
├───────────────────┤ ├──────────────────────────┤
│ state │ │ update(event) │
│ setState(s) │ └──────────────────────────┘
└───────────────────┘

The diamond is aggregation — Subject holds a list of Observer references but does not own their lifetimes.

When to use it#

Reach for Observer when the answer to every one of these is yes:

  • An object’s state changes and multiple other objects need to react.
  • The set of reactors changes at runtime (subscribers come and go).
  • The subject should not need to know who the reactors are — adding a new kind of reactor must not edit the subject.
  • The reactions are best-effort and independent of each other — observer A failing should not block observer B.

Common scenarios:

  • UI binding — a model changes; views render.
  • Event-driven domain logicOrderPlaced fires; inventory adjusts, emails go out, analytics records.
  • Reactive streams — a source emits items; multiple consumers process them.
  • Multi-listener telemetry — a service exposes a hook; logs, traces, metrics, and SIEM all subscribe.

When not to use it:

  • When you need ordered, atomic, transactional propagation. Observers run as a best-effort sequence; “notify all or none” requires a transaction outbox, not an in-process Subject.
  • When there is exactly one reactor and the relationship will not grow — direct method call is simpler and clearer.
  • When you need back-pressure. Observer is a push pattern; if observers can’t keep up, you need a queue, not the GoF Observer.

How it works#

A minimal stock-ticker example. The subject holds a list of observers and pushes the new price on every change. Observers are anonymous to the subject — only the interface is.

import java.util.ArrayList;
import java.util.List;
public interface PriceObserver {
void onPriceChanged(String symbol, double newPrice);
}
public final class StockTicker {
private final String symbol;
private double price;
private final List<PriceObserver> observers = new ArrayList<>();
public StockTicker(String symbol, double openingPrice) {
this.symbol = symbol;
this.price = openingPrice;
}
public void attach(PriceObserver o) {
observers.add(o);
}
public void detach(PriceObserver o) {
observers.remove(o);
}
public void setPrice(double newPrice) {
if (newPrice == this.price) return; // do not notify on no-op
this.price = newPrice;
notifyAllObservers();
}
private void notifyAllObservers() {
// Defensive copy — listeners may detach themselves during dispatch.
for (PriceObserver o : new ArrayList<>(observers)) {
try {
o.onPriceChanged(symbol, price);
} catch (RuntimeException ex) {
// One observer's failure must not abort the rest.
System.err.println("Observer " + o + " threw: " + ex);
}
}
}
}
public final class PriceLogger implements PriceObserver {
@Override public void onPriceChanged(String symbol, double newPrice) {
System.out.println("[log] " + symbol + " = " + newPrice);
}
}
public final class AlertAtThreshold implements PriceObserver {
private final double threshold;
public AlertAtThreshold(double threshold) { this.threshold = threshold; }
@Override public void onPriceChanged(String symbol, double newPrice) {
if (newPrice >= threshold) System.out.println("[alert] " + symbol + " hit " + threshold);
}
}
// Wiring:
// StockTicker t = new StockTicker("ACME", 100.0);
// t.attach(new PriceLogger());
// t.attach(new AlertAtThreshold(150.0));
// t.setPrice(149.5);
// t.setPrice(151.2);

Five non-obvious details earn their keep in this implementation:

  • Defensive copy before dispatch. An observer’s update may call detach on itself — iterating the live list would throw ConcurrentModificationException. Copy first, iterate the copy.
  • No-op guard. setPrice(samePrice) does not fire. Without this, listeners receive a flood of redundant events.
  • Per-observer exception isolation. A buggy observer must not break the dispatch loop or hide the failure from siblings.
  • No global state. The subject owns its observer list. The pattern does not require a global event bus to work; that is a different (and looser) architecture.
  • Tracking by reference. detach uses object identity. If observers are anonymous lambdas you cannot detach later, you have created a leak — see Variants below.

Variants#

VariantMechanismWhen it fits
PushSubject calls update(event) with the full payload.Few observers; small payload; observers usually want everything.
PullSubject calls update(this); observer queries the subject for what it cares about.Many observers, each interested in a subset; subject state is expensive to bundle.
Topic / event-busObservers subscribe to event types, not a single subject. A bus dispatches.Many subjects, many observers, cross-cutting concerns.
Reactive streamsObservers are operators in a chain; back-pressure is part of the contract.Throughput-bound pipelines; consumers may stall.
Weak referencesSubject holds WeakReference<Observer>.Long-lived subjects with shorter-lived observers (UI views, request handlers).

The push/pull distinction is the most common interview probe. The example above is push. Pull would change onPriceChanged(String, double) to onPriceChanged(StockTicker), and the observer would read whatever it needs from the subject.

Example systems#

The pattern shows up at every layer of the stack:

  • Spreadsheets — cell formulas observe the cells they reference. Editing A1 notifies B1=A1+1, which notifies C1=B1*2.
  • Newsfeeds — a publisher posts; subscribers materialise the new item into their feed.
  • Java’s deprecated Observable — the original implementation, retired in JDK 9 for the reasons under “Trade-offs” below.
  • Reactive libraries — RxJava Observable, Project Reactor Flux, the JDK Flow API. Same shape, with back-pressure attached.
  • Browser DOMaddEventListener / removeEventListener is Observer with the bus baked into the platform.

Trade-offs#

What you gain:

  • Decoupling. The subject depends only on the observer interface, not on any concrete observer. New observer types can be added without editing the subject — a clean application of the Open-Closed Principle.
  • Runtime flexibility. Observers can be attached and detached as the program runs.
  • Broadcast for free. Adding the fifth listener costs the same as adding the first.

What you pay:

  • The leaky-listener problem. If observers are attached and never detached, the subject keeps them alive — a memory leak that is hard to diagnose because the references are “logically” gone. Mitigations: explicit detach in observer teardown, WeakReference-based subjects, scoped subscriptions (AutoCloseable).
  • Update storms. A single state change can fan out into thousands of cascading notifications if observers themselves are subjects. Coalescing (batch and flush) is a frequent retrofit.
  • Order is implicit. Observers run in attachment order by default. If observer B depends on a side-effect of observer A, the architecture has a hidden ordering constraint that the next refactor will break.
  • Debugging fan-out is painful. A stack trace through twelve observers does not tell you why the chain started; structured logging at the subject is non-negotiable.
  • Thread safety is your problem. The pattern as stated is single-threaded. Multi-threaded subjects need locking on the observer list and a policy for whether notify runs on the publisher’s thread or hands off.
  • Strategy Pattern — both use composition over inheritance, but Strategy has one collaborator at a time and a synchronous return; Observer has many and is fire-and-forget.
  • State Pattern — a State object can be an Observer of the context it lives in; the patterns compose cleanly when state transitions need to fan out.
  • Command Pattern — Command captures what to do; Observer captures when. A notification often contains a Command for the observer to execute later.
  • Mediator pattern (not covered in this workbook) — Mediator centralises many-to-many wiring that naive Observer turns into a graph.
  • Open Closed Principle (OCP) — Observer is the OCP refactor for “we keep adding new things that need to react to this event.”
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.