Observer Pattern: Notifying Many Listeners Safely
How one object notifies many others without knowing them. Java code shows the pattern and its classic bug: removing a listener mid-notification silently skipped the next one.
The observer pattern is a one-to-many dependency where a subject keeps a list of registered listeners and notifies each one when its state changes, without knowing their concrete types. Interviewers expect the classic bug: a listener that removes itself during notification can make a plain list skip the next listener or throw ConcurrentModificationException, so iterate over a copy.
The observer pattern lets a subject publish events to any number of listeners that register with it, keeping them decoupled. The common failure is modifying the listener list while notifying: in a Java test, a listener that removed itself caused the next listener to be silently skipped, until the loop iterated over a copy.
- In the observer pattern, a subject exposes add and remove methods, stores listeners as an interface type, and calls each one when an event occurs.
- In a Java test with two listeners on an ArrayList, a listener that removed itself during notification caused the second listener to be skipped with no exception.
- Iterating over a snapshot such as new ArrayList<>(ls), or using CopyOnWriteArrayList, delivers each event to every listener registered when it fired.
- The lapsed listener problem is a memory leak: a subject keeps every registered listener alive until the listener is explicitly removed.
- In the observer pattern observers register directly with the subject, while publish-subscribe puts a broker between publishers and subscribers.
- Java's java.util.Observable has been deprecated since Java 9 because its design is rigid and not thread-safe.
01The idea
Sometimes one object's state change must trigger reactions in several others: a model updating several views, a button notifying handlers, a price feed notifying traders. If the changing object calls each interested object directly, it must know all of them, and every new reaction means editing it.
The observer pattern reverses that. The subject exposes add(listener) and remove(listener). It keeps the listeners as a list of an interface type and, when something happens, calls each one. Listeners decide for themselves what to do. The subject depends only on the small Listener interface, so listeners can be added or removed without changing it. This is the core of event systems, GUI toolkits and reactive libraries.
02A minimal version
interface Listener { void on(String event); }
static class Subject {
final List<Listener> ls = new ArrayList<>();
void add(Listener l) { ls.add(l); }
void remove(Listener l) { ls.remove(l); }
void fire(String e) {
for (Listener l : ls) l.on(e);
}
}Usage is one line per listener, and lambdas make it light: subject.add(e -> System.out.println("got " + e));. This works until a listener changes the list during a notification. That is more common than it sounds: a "one-shot" listener that unsubscribes itself after the first event is a normal need.
03The classic bug: modifying while notifying
I registered two listeners. The first printed a message and then removed itself. The second printed its own message. Calling fire("A") with the loop above printed only once got A. The second listener never ran, and no exception was thrown.
What happened is a quirk of the ArrayList iterator. After the first element removed itself the list had size 1, the iterator's cursor was also 1, and its hasNext() check (cursor != size) returned false, so the loop ended early. With three or more listeners the same pattern typically throws ConcurrentModificationException instead, which is at least visible. The silent version is worse: an event was lost and nothing says so.
04The fix: iterate over a snapshot
void fire(String e) {
for (Listener l : new ArrayList<>(ls)) l.on(e); // snapshot
}With this, fire("A") printed once got A then second got A, and a later fire("B") printed only second got B, because the one-shot had gone. Every listener registered at the time of the event got it, and the removal took effect for the next event. CopyOnWriteArrayList does this automatically and is thread-safe for iteration, a good fit when listeners change rarely and events are frequent. Decide and document what a listener added during notification should receive: with a snapshot, it misses the current event.
05Other problems to plan for
Lapsed listener leak: the subject holds a reference to every listener, so a short-lived object that registers and never unregisters stays in memory as long as the subject lives. Provide an unsubscribe path, return a handle or Closeable from add, or hold listeners through weak references where that fits.
Exceptions: if one listener throws, the loop stops and later listeners are skipped. Decide whether to catch and log per listener so one bad observer cannot break the rest.
Order: do not rely on listeners being called in registration order unless you document it. Re-entrancy: a listener that triggers another event inside its callback can cause deep or looping notifications.
Threads: if events are fired from one thread and listeners are added from another, you need a thread-safe list, and you must decide which thread runs the callbacks (GUI toolkits require the UI thread).
06Push, pull, and relatives
In a push model the subject sends the changed data in the event. In a pull model it only says "something changed" and listeners query the subject for what they need. Push is simpler for the listener, pull avoids sending data nobody wants.
Related ideas: publish-subscribe puts a broker or event bus between publishers and subscribers so they do not know each other at all, and adds topics and often asynchronous delivery. Reactive streams add back-pressure for fast producers. Java's older java.util.Observable is deprecated since Java 9, as its design is rigid and not thread-safe, and the standard advice is to write the small interface above or use PropertyChangeListener or a library.
07Common interview questions
Q: What is the observer pattern?
A: A one-to-many dependency where a subject notifies registered observers of changes without knowing their concrete types.
Q: Where is it used?
A: GUI event listeners, model-view updates, event buses, and callbacks in many libraries.
Q: What goes wrong when a listener removes itself during notification?
A: With a plain list you get a ConcurrentModificationException or, as in the two-listener case, a silently skipped listener. Iterate over a copy.
Q: What is the lapsed listener problem?
A: Registered listeners are never removed, so the subject keeps them alive and causes a memory leak.
Q: Observer versus publish-subscribe?
A: In observer, observers register directly with the subject. In pub-sub, a broker decouples publishers and subscribers completely.
Q: Push versus pull?
A: Push sends the data with the event. Pull sends a notification and observers fetch what they need.