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 knowing who they are or what they will do.

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 and trade off bandwidth for flexibility.
  • 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