Skip to main content

Observer: Publishing State Changes Without Coupling

What This Concept Is​

Observer lets an object (the subject or publisher) notify other objects (observers or subscribers) of state changes, without depending on concrete observer classes. Payload, timing, ownership and failure contracts still couple the participants.

Three moving parts:

  • a subject with subscribe, unsubscribe, and notify
  • an observer interface with a single update method
  • a wiring step at startup (or dynamically) that connects observers to subjects

The coupling direction matters: the subject depends only on the observer interface. Concrete observer classes can be added later without touching the subject.

Why It Matters Here​

Many systems have one source of truth and many things that must react: a domain model and UI widgets, a config and caches that depend on it, a job and its progress listeners.

Without Observer, the source starts calling each listener by name. That direct call list is the coupling that bites you six months later when a new listener type shows up and the publisher has to change.

Concrete Example​

A weather station publishing temperature updates to multiple displays.

interface Observer { void update(double tempC); }

class WeatherStation {
private final List<Observer> observers = new ArrayList<>();
private double tempC;

public void subscribe(Observer o) { observers.add(o); }
public void unsubscribe(Observer o) { observers.remove(o); }

public void setTemperature(double t) {
if (t == tempC) return;
tempC = t;
notifyAll(t);
}

private void notifyAll(double t) {
for (Observer o : new ArrayList<>(observers)) {
o.update(t);
}
}
}

class ConsoleDisplay implements Observer {
public void update(double tempC) {
System.out.printf("now %.1f C%n", tempC);
}
}

class AlertService implements Observer {
public void update(double tempC) {
if (tempC > 35.0) System.out.println("HEAT ALERT");
}
}

WeatherStation ws = new WeatherStation();
ws.subscribe(new ConsoleDisplay());
ws.subscribe(new AlertService());
ws.setTemperature(36.2); // both react; WeatherStation knows neither class

WeatherStation never imports ConsoleDisplay or AlertService. A new GraphDisplay observer tomorrow is zero change here.

Common Confusion / Misconception​

  • "Observer is just callbacks." Technically yes, but the pattern's value is in the explicit subscribe/unsubscribe and the stable update interface that decouples types.
  • Notifying synchronously feels implicit. The call stack runs through every observer. If one blocks, the subject blocks. This is a feature (ordering, exceptions propagate) but bites under load -- see the next concept for pitfalls.
  • Push-only mindset. The update method can pass the new value directly (push) or pass the subject so the observer pulls what it needs. Both are valid; compare snapshot consistency, access boundaries and data volume. In-process push can pass a reference without copying the whole object.
  • Observer is not the same as Pub/Sub. Observer is object-to-object inside a process. Pub/Sub usually involves a broker, topics, and often a network boundary. Related, not identical.

How To Use It​

  1. Identify the "one writer, many readers" shape.
  2. Define a narrow observer interface (update, or domain-named method).
  3. On the subject, expose subscribe, unsubscribe, and a guarded notify that walks a copy of the list.
  4. Decide push vs pull: push simple values, pull when observers need different slices.
  5. Document re-entrancy guarantees (can an observer subscribe or unsubscribe during update?).
  6. Wire subjects to observers at composition time; never hardcode them in the subject.

Check Yourself​

  1. Which direction does the dependency between subject and observer point?
  2. Why walk a copy of the subscriber list during notification?
  3. What is the difference between push and pull notification?
  4. How do you add a new kind of observer without modifying the subject?

Mini Drill or Application​

Take a logger that currently writes to stdout and also writes to a file via duplicated calls scattered in the caller.

  1. Extract a LogEvent value and a LogSink (observer) interface.
  2. Make the logger the subject with subscribe(LogSink).
  3. Add a StdoutSink, a FileSink, and a MemorySink for tests.
  4. Show that adding a NetworkSink later requires zero changes in the logger.

Stop when the logger imports only the sink interface.

Read This Only If Stuck​

Contract and transfer​

The Java block is a class sketch: it needs java.util imports and its wiring statements inside an entry method to compile. Temperature starts at 0, so the first setTemperature(0) does not notify. The snapshot preserves membership during this call, but the first uncaught observer exception stops later callbacks after the temperature has already changed. This is a fail-fast, nontransactional policy, not a claim that Observer makes failures independent.

L02 chooses a different explicit policy: collect ordinary callback failures and continue, reject nested publication, and return the failures to the publisher. Registering the same callback twice creates two independent subscriptions. A disposal handle identifies one registration, so disposing it twice must not remove the other registration. An unsubscribe handle still has to be called by its owner.

Transfer: if a screen disposes listener B during listener A, should B receive the current event? Both answers are defensible contracts. Predict and test the chosen answer. A snapshot of listener membership does not copy the payload: mutable payload changes can still be observed by later listeners.