Observer Pattern
1. What the observer pattern is
The observer pattern lets one object broadcast its state changes to a set of dependents without knowing who they are. The object that changes is the subject; the dependents are observers. The subject keeps a list of observers and, whenever its state changes, walks the list and tells each one. Their concrete types stay hidden from it.
Three roles:
- Subject — holds state, holds a list of observers, exposes
subscribe/unsubscribe, and calls every observer when state changes. - Observer — an interface with one method the subject calls, e.g.
update(...). - Concrete observer — implements that method to react however it wants.
The dependency runs one way. Observers depend on the subject’s Observer interface; the subject depends on nothing. Add a new observer and the subject is untouched.
2. Why notify observers instead of polling
The alternative to being notified is asking. Without the pattern, anything that cares about the subject’s state has to poll it: re-read the value on a timer or in a loop and diff against the last value it saw. That is wasteful when nothing changed and late when something did.
Hard-wiring the calls is the other naive option. The subject calls each dependent directly:
public void setTemperature(int t) {
this.temperature = t;
display.refresh(t);
logger.record(t);
alarm.check(t);
}This compiles and works. It also means the subject imports Display, Logger, and Alarm, and every new dependent edits setTemperature. The subject now knows its entire audience.
The observer pattern cuts that. The subject holds a List<Observer> and nothing else:
- Decoupling. The subject depends on one interface. New observer, no change to the subject.
- Runtime wiring. Observers come and go while the program runs. A display can subscribe when it is shown and unsubscribe when it is hidden.
- Fan-out. One state change, N reactions, and the subject doesn’t count them.
The cost is indirection. You can no longer read setTemperature and see what happens next. §6 is about when that trade isn’t worth making.
3. Writing one in Java
The example is a weather station. The subject is a sensor that holds a temperature; observers are things that react to it, such as a display and a log.
3.1. The Observer interface and the subject
The observer interface is one method:
public interface Observer {
void update(int temperature);
}The subject manages the observer list and the notification:
public class WeatherStation {
private final List<Observer> observers = new ArrayList<>();
private int temperature;
public void subscribe(Observer observer) {
observers.add(observer);
}
public void unsubscribe(Observer observer) {
observers.remove(observer);
}
public void setTemperature(int temperature) {
this.temperature = temperature;
notifyObservers();
}
private void notifyObservers() {
for (Observer observer : List.copyOf(observers)) {
observer.update(temperature);
}
}
}The List.copyOf(observers) in notifyObservers is not decoration. An observer’s update may unsubscribe itself or someone else, which mutates observers mid-iteration and throws ConcurrentModificationException. Iterating a snapshot makes the notification immune to that. This is the concurrent-modification half of the re-entrancy problem covered in §4.2, handled at the source.
3.2. Concrete observers
A concrete observer implements update and reacts:
public class TemperatureDisplay implements Observer {
@Override
public void update(int temperature) {
System.out.println("Display: " + temperature + "°C");
}
}The client wires it together:
WeatherStation station = new WeatherStation();
station.subscribe(new TemperatureDisplay());
station.subscribe(new TemperatureLog());
station.setTemperature(21); // both observers fireWeatherStation never mentions TemperatureDisplay or TemperatureLog. That is the decoupling from §2: the subject depends on the Observer interface and nothing else.
3.3. Push vs pull
update(int temperature) pushes: the subject hands the changed value straight to the observer. The alternative is pull, where the subject passes itself and the observer reads what it needs:
public interface Observer {
void update(WeatherStation station);
}@Override
public void update(WeatherStation station) {
int t = station.getTemperature();
// react to t
}Push is simpler and keeps the observer ignorant of the subject’s type, but it bakes the payload into the interface: the day an observer also needs humidity, every observer’s update signature changes. Pull keeps the interface stable (update(subject) never changes) at the cost of coupling observers to the subject’s getters and letting them read state you didn’t mean to expose.
Pick push when the subject has one or two stable values to report. Pick pull when observers need different and changing slices of a larger state. I used push above because a weather reading is one number. The moment it became a dozen fields I’d switch.
4. Three traps
The pattern is small, but three things bite in real code: observers that outlive their usefulness and leak memory, notifications that re-enter the subject mid-broadcast, and observers that quietly depend on the order they fire in.
4.1. Lapsed listeners leak memory
subscribe hands the subject a strong reference to the observer. As long as the subject is alive, every observer it holds is reachable, so the garbage collector cannot reclaim them. An observer that is logically dead (a closed window, a disposed component) but that nobody unsubscribed lives as long as the subject does. This is the lapsed-listener problem, and it is the most common observer-pattern memory leak.
The fix is discipline first: whoever subscribes an observer is responsible for unsubscribing it. When that is impractical, hold observers through WeakReference so the subject’s reference does not keep them alive, and drop cleared references during notification. The complexity weak references introduce mean you should reach for them only when ownership genuinely cannot be pinned down.
4.2. Re-entrant notification
An observer’s update can call back into the subject, subscribe, unsubscribe, or trigger another state change while notifyObservers is still running. The snapshot in §3.1 handles the structural half (the list will not change under the iterator), but it does not stop a re-entrant setTemperature from starting a second notification nested inside the first. Observers can then see updates out of order, or the calls recurse until the stack overflows. Keep update methods free of calls that mutate the subject; if a reaction must change subject state, defer it by queuing it or posting it to run after the notification finishes.
4.3. Observers must not assume an order
notifyObservers iterates a List, so observers fire in subscription order today. That is an accident of the implementation, not a contract. If a TemperatureLog must run before a TemperatureDisplay, encoding that as “subscribe the log first” is a bug waiting for the day someone reorders two lines. When observers genuinely have a dependency between them, that is a sign they should not be peers: chain them explicitly, or insert an intermediate observer that calls them in the order it owns.
5. What Java ships instead of rolling your own
Java had a built-in java.util.Observable class and java.util.Observer interface since 1.0. Both were deprecated in Java 9. Don’t use them. Observable is a class, not an interface, so your subject had to extend it and gave up its one inheritance slot; its change tracking (setChanged/hasChanged) was clumsy and not thread-safe; and the payload was an untyped Object. The §3 hand-rolled version is half a screen of code and better than what Observable offered.
What to use instead depends on the case:
java.beans.PropertyChangeSupport— the observer pattern for “a property changed”, with no deprecation baggage. Your subject holds aPropertyChangeSupport, delegatesaddPropertyChangeListenerto it, and callsfirePropertyChange("temperature", oldValue, newValue). Listeners receive aPropertyChangeEventcarrying the old and new values. It owns the list and the iteration for you.- The listener idiom — what GUI toolkits use: a typed
XxxListenerinterface,addXxxListener/removeXxxListenermethods, and anXxxEventpayload object. It is the §3 pattern with naming conventions. When you need more than one kind of event, this scales better than a bareObserver. java.util.concurrent.Flow(Java 9+) — the reactive-streams interfaces (Publisher,Subscriber,Subscription). This is the observer pattern with backpressure: the subscriber controls how much it receives viaSubscription.request(n). Reach for it when observers can be overwhelmed by a fast subject. For a temperature that changes every few seconds, it is overkill.
Hand-roll the §3 version for something small and self-contained. Use PropertyChangeSupport or the listener idiom the moment the subject is part of a larger system. Save Flow for genuine streaming.
6. When not to reach for it
The observer pattern trades a direct, readable call for runtime indirection. When the indirection buys nothing, it is pure cost. A few smells:
- One observer, fixed for the life of the program. If exactly one thing reacts and it is wired once at startup, a direct call is clearer.
notifyObserversover a one-element list is ceremony. - The set of reactions is known and stable. If observers never come and go at runtime and the list never grows, the hard-wired calls from §2 are honest about what happens. The pattern earns its keep when wiring is dynamic.
- You need the result of the reaction.
updatereturnsvoid. The observer pattern is fire-and-forget broadcast; if the subject must collect answers from its dependents, that is a different shape (a pipeline, a strategy applied to each), not an observer.
The pattern pays off when a subject has a changing audience it should not know about: dependents that subscribe and unsubscribe while the program runs, and a count the subject has no business tracking. Short of that, calling the dependent directly is fewer moving parts and a stack trace you can actually read.