Observer Pattern
Learn to decouple a data source from everything that reacts to it, by having any number of independent listeners subscribe to a shared notification contract instead of being hardwired in.
Learning objectives
- Explain why hardwiring a data source to a specific listener type violates the Open/Closed Principle
- Design a shared Observer contract that lets any number of unrelated classes subscribe generically
- Implement attach, detach, and notify on a Subject without knowing concrete observer types
- Recognize memory-leak and error-propagation risks in a notification loop
- Identify where publish-subscribe notification appears in GUI frameworks and the Java standard library
◆ Story
A magazine publisher doesn't know your name, your address preferences, or how many other magazines you subscribe to elsewhere. It knows exactly one thing: whenever a new issue is ready, mail a copy to everyone currently on the subscriber list. Subscribers can join or leave that list whenever they want, completely independently of how the publisher actually prints and assembles each issue.
That's the whole shape of the Observer pattern: one source of truth, and any number of independent parties that want to know when it changes, without the source needing to know who they are or how many of them there are.
A weather station makes the same problem concrete. It records a temperature reading, and whenever that reading changes, every connected display should update immediately — a wall-mounted screen today, a phone app tomorrow, and the station shouldn't need to be rewritten every time a new kind of display shows up. Building that requirement the obvious way first, and watching exactly why it breaks as soon as a second display appears, is what makes the eventual fix click.
Notice that this is a different flavor of problem than the one solved by encapsulating undo history: there, one object needed to remember its own past states privately. Here, one object needs to broadcast its present state to any number of outside parties, without ever being told in advance who those parties will be. Both problems share a common root — an object doing too much on its own, when the real fix is delegating a responsibility to a class built specifically for it — but the shape of the fix looks quite different once it's built out.
The obvious starting point: the station keeps a direct reference to its one display device, and pushes the new reading to it the moment the temperature changes. A WeatherStation constructor takes a DisplayDevice, stores it, and calls a showTemperature() method on it directly from inside setTemperature().
Run it and it works cleanly for exactly one display. So is this fine? The real requirement lands the moment a second display — say, a mobile app — needs to show the same reading. Adding it means giving WeatherStation a second field, a second constructor parameter, and a second line inside its notification method — editing a class that's already shipped and already trusted, purely because the roster of interested displays grew by one.
That's a direct violation of the Open/Closed Principle: the station was already working, already tested code, and every new display type forces it open again for modification. It gets worse as more display types show up. A tablet, a smartwatch face, a logging service that just writes readings to a file — each one means another field and another line inside the notification method, with nothing about this design putting a ceiling on how far that grows. And because the device is wired in through the constructor, there's no way to say "stop notifying this one" without changing code, recompiling, and redeploying — devices can't be added or removed while the program is running.
The station currently has to know the concrete type of every single thing that might ever want its readings. That's backwards. A data source shouldn't need to know who's listening, how many listeners there are, or what any of them individually do with the value once they get it.
💻 Code example
class DisplayDevice { void showTemperature(float temp) { System.out.println("Current temperature is: " + temp + " Celsius"); } } class WeatherStation { private float temperature; private DisplayDevice displayDevice; // hardwired to ONE specific concrete device WeatherStation(DisplayDevice displayDevice) { this.displayDevice = displayDevice; } void setTemperature(float temp) { this.temperature = temp; notifyDevice(); } void notifyDevice() { displayDevice.showTemperature(this.temperature); // only ever knows about this one device } } // Works for one device. Adding a second display means editing WeatherStation // itself — a new field, a new constructor parameter, a new line in notifyDevice().
Reason through the fix before writing any new class. The station shouldn't hold a specific device type at all — it should hold a list of "things that want to be notified," with zero knowledge of what any of them actually are underneath. Every listener needs to expose exactly one shared method — something like "here's the new value, do whatever you need to with it" — so the station can call it generically on anything in that list. And that list needs to be changeable while the program is running, so devices can join and leave without the station's own code ever being touched.
This is the Observer pattern: a Subject maintains a list of Observers and notifies all of them, generically, whenever its own state changes, with zero knowledge of what any specific observer actually does with that notification.
Start with the contract every listener must satisfy — deliberately just one method. Then define the Subject side: attach, detach, and a method to notify everyone currently attached. Subject is written as an interface here rather than an abstract class on purpose. Java allows a class to extend only one parent, so if some future subject already needs to extend something else, forcing it through an abstract base class would block that entirely. An interface stays adoptable by any class, no matter what else it already inherits from.
Rebuild the weather station against these two contracts. It now holds a List<Observer> instead of one concrete DisplayDevice. attach() and detach() add and remove observers from that list at runtime — no constructor involved, no recompiling needed. notifyObservers() loops over the current list and calls the shared update() method on each one generically; it has no idea what concrete type any given observer actually is. Compare that to the old notifyDevice(): that method had to change every time a new device type appeared. This one never changes again, no matter how many observer types get added later, because it only ever calls the one method every observer is guaranteed to have.
💻 Code example
interface Observer { void update(float temperature); // the ONE thing every observer must be able to do } interface Subject { void attach(Observer observer); void detach(Observer observer); void notifyObservers(); } class WeatherStation implements Subject { private float temperature; private final List<Observer> observers = new ArrayList<>(); // a LIST, not one hardwired field public void attach(Observer observer) { observers.add(observer); } // join at runtime public void detach(Observer observer) { observers.remove(observer); } // leave at runtime public void notifyObservers() { for (Observer o : observers) { o.update(this.temperature); // generic call — no idea what kind of observer this is } } void setTemperature(float temp) { this.temperature = temp; notifyObservers(); } }
Build two independent observer types to prove the design actually works: a DisplayDevice implementing Observer, and a completely unrelated MobileDevice also implementing Observer. Neither class knows the other exists. Neither extends anything related to weather. WeatherStation never mentions either class by name anywhere in its own source — the only thing connecting all three is the shared Observer contract.
| After this line... | observers | Who prints on next setTemperature |
|---|---|---|
attach(lcd) | [lcd] | lcd only |
attach(mobile) | [lcd, mobile] | lcd, mobile |
setTemperature(25) | unchanged | both print 25.0 |
detach(mobile) | [lcd] | lcd only |
setTemperature(26) | unchanged | lcd prints 26.0, mobile prints nothing |
The detach call is worth sitting with: mobile the object still exists in memory — nothing was destroyed — it simply isn't in the list anymore, so notifyObservers()'s loop never reaches it again. That's the entire mechanism behind "unsubscribe," and it generalizes to any number of observer types without a single change to WeatherStation. A third display type — a smartwatch, a logging service, anything at all — is just one new class implementing Observer and one attach() call; nothing about WeatherStation.notifyObservers() needs to know it exists.
Compare this trace against the earlier hardwired version one more time: there, adding the mobile device meant a second constructor parameter and a second field on WeatherStation itself. Here, adding it is a runtime call on an already-constructed station — the difference between a change that requires editing and redeploying a class, and a change that's just ordinary program execution.
💻 Code example
class DisplayDevice implements Observer { private final String name; DisplayDevice(String name) { this.name = name; } public void update(float temperature) { System.out.println("Temperature on " + name + " is: " + temperature); } } class MobileDevice implements Observer { public void update(float temperature) { System.out.println("Temperature on mobile is: " + temperature); } } public class Main { public static void main(String[] args) { WeatherStation station = new WeatherStation(); DisplayDevice lcd = new DisplayDevice("Samsung LCD"); MobileDevice mobile = new MobileDevice(); station.attach(lcd); station.attach(mobile); station.setTemperature(25); // both devices print automatically station.detach(mobile); station.setTemperature(26); // only lcd prints now } }
▲ Edge case — forgetting to detach causes real memory leaks
As long as WeatherStation is alive, its observers list keeps every attached observer reachable, and everything that observer itself holds onto, transitively. A UI component that gets destroyed without ever calling detach() stays alive in memory anyway, invisibly, for as long as the station does. Always pair a subscribe with a clear, enforced plan for unsubscribing.
▲ Edge case — one misbehaving observer can break every other observer
notifyObservers()'s loop calls update() on each observer in sequence. If one observer's update() throws an uncaught exception, the loop stops right there, and every observer later in the list never gets notified — for a reason that has nothing to do with them. Production implementations typically wrap each call in a try/catch inside the loop, logging the failure without letting one bad observer silently starve the rest.
▲ Edge case — notification order is rarely a guarantee
Using an ArrayList means notification happens in attach order, but that's an implementation detail, not something the pattern promises. Code that depends on "device A always hears about this before device B" is fragile — if the underlying collection or notification strategy ever changes, say to notify observers concurrently on separate threads for performance, that assumption silently breaks.
▲ Edge case — synchronous notification means the Subject waits on every Observer
notifyObservers() as built is synchronous: setTemperature() doesn't return until every observer's update() has finished running. A slow observer — one that writes to disk or calls a network service — blocks the Subject and every observer behind it in the list. High-throughput publish-subscribe systems solve this by making notification asynchronous instead, a genuinely different trade-off worth knowing exists even when a simple in-process example doesn't need it.
◆ Where this pattern actually shows up
- GUI event listeners — a button is a Subject; every registered click handler is an Observer. Java's own Swing and AWT frameworks (
ActionListener,MouseListener) are built directly on this shape. java.util.Observer/Observable— Java's standard library shipped this pattern directly from version 1.0, though both types were deprecated in Java 9 for being too limited (no proper event ordering,Observablehad to be extended rather than composed). The reactivejava.util.concurrent.FlowAPI, added in Java 9, is the modern replacement for the same underlying idea.- Stock price and sensor monitoring — one data source, many independent subscribers displaying or reacting to the same feed.
- Property change notification —
java.beans.PropertyChangeListenerlets any bean notify listeners when a named property changes, a typed variant of exactly this pattern. - Logging systems — a single event can be observed by a console logger, a file logger, and a remote monitoring service simultaneously, none of them aware the others exist.
Q: Why does hardwiring a Subject directly to a specific Observer type violate the Open/Closed Principle? : Every new observer type requires editing the Subject's own already-tested code — a new field, a new notify call — instead of the Subject staying closed to modification while new observers plug in freely from outside.
Q: What does the Subject actually know about its Observers? : Only that each one satisfies the Observer interface — nothing about their specific concrete types, or what any of them individually do with a notification.
Q: What's a common real bug caused by not properly detaching an observer? : A memory leak — the Subject's list keeps a reference to the observer, and everything it holds, alive even after the rest of the application no longer needs it.
Q: Why is Subject typically written as an interface rather than an abstract class? : Java allows extending only one class, so an abstract Subject base class would block any concrete subject that already needs to extend something else. An interface can be adopted by any class regardless of its existing hierarchy.
Q: What happens if one observer throws an exception during notifyObservers(), and how is that usually fixed? : Without individual error handling inside the loop, the exception propagates and stops the loop entirely — every observer later in the list never gets notified. Wrapping each observer's update() call in its own try/catch, logging failures, prevents one bad observer from starving the rest.
Want a visual for this concept?
Generate a diagram tailored to “Observer Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →