The subject-notifies-observers pattern behind most event systems — push vs pull models, and the classic memory leak from un-removed observers.
Published September 23, 2026
A subject maintains a list of observers and notifies all of them when its state changes — neither side needs to know the other's concrete type, only the shared Observer contract.
interface StockObserver { void onPriceChange(String symbol, double newPrice); }
class StockPriceTracker { // the subject
private final List<StockObserver> observers = new ArrayList<>();
private double price;
void subscribe(StockObserver o) { observers.add(o); }
void unsubscribe(StockObserver o) { observers.remove(o); }
void updatePrice(String symbol, double newPrice) {
this.price = newPrice;
for (StockObserver o : observers) o.onPriceChange(symbol, newPrice); // notify everyone
}
}
class DisplayPanel implements StockObserver {
public void onPriceChange(String symbol, double price) { System.out.println(symbol + ": " + price); }
}
class PriceAlertService implements StockObserver {
public void onPriceChange(String symbol, double price) { if (price > 100) triggerAlert(symbol); }
private void triggerAlert(String symbol) { /* ... */ }
}
StockPriceTracker tracker = new StockPriceTracker();
tracker.subscribe(new DisplayPanel());
tracker.subscribe(new PriceAlertService());
tracker.updatePrice("AAPL", 150.0); // both subscribers react independently
Neither DisplayPanel nor PriceAlertService know about each other, and StockPriceTracker doesn't know what its observers actually do with the notification — it just broadcasts. This is the foundation almost every event/pub-sub system builds on, from GUI listeners to message queues.
The example above is push: the subject sends the full new state (symbol, newPrice) directly in the notification. The alternative is pull: the subject only sends a minimal "something changed" signal, and the observer calls back into the subject to fetch whatever specific data it needs.
interface PullObserver { void onUpdate(StockPriceTracker subject); } // subject reference, not data
class DisplayPanel implements PullObserver {
public void onUpdate(StockPriceTracker subject) {
double price = subject.getCurrentPrice(); // observer decides what it needs, and pulls it
}
}
Push is simpler and cheaper when every observer wants roughly the same data. Pull is more flexible when different observers care about different subsets of state, avoiding the subject having to know every possible thing an observer might want.
class ExpensiveService {
ExpensiveService(StockPriceTracker tracker) {
tracker.subscribe(this::onPriceChange); // subscribed, but never unsubscribed
}
private void onPriceChange(String s, double p) { /* ... */ }
}
If ExpensiveService instances are meant to be short-lived but the StockPriceTracker is long-lived, every ExpensiveService that never calls unsubscribe() stays referenced forever by the tracker's observer list — the garbage collector can't reclaim it, since the tracker still holds a live reference. This is a textbook listener/callback leak (also called out in Memory Leaks in Java): the fix is either explicit unsubscription in a lifecycle hook (close(), @PreDestroy), or using weak references for the observer list so the GC can collect observers that nothing else holds onto.
Q: Is Java's own PropertyChangeListener an Observer implementation?
A: Yes — java.beans.PropertyChangeSupport is essentially a built-in Observer subject, and Swing/AWT's event listener model (ActionListener, etc.) follows the same shape throughout the JDK.
Q: How does Observer relate to a message queue / event bus in a backend system? A: Same core idea at a different scale and with different delivery guarantees — a message queue decouples publisher and subscriber across processes/services (with persistence, retry, and ordering concerns Observer doesn't address in-process), but the fundamental "notify interested parties without either side knowing the other's concrete type" relationship is identical.
Q: What happens if an observer's callback throws an exception during notification?
A: In the naive loop shown above, an exception in one observer's onPriceChange would propagate up and prevent any later observers in the list from being notified at all — production Observer implementations typically wrap each observer call in its own try/catch so one misbehaving observer can't break notification for the rest.
Q: Would you use push or pull for a UI that shows a live dashboard of many different metrics? A: Pull tends to fit better there — different dashboard widgets likely care about different subsets of the subject's state, and pushing every possible field to every observer regardless of relevance wastes both bandwidth and each observer's effort filtering out data it doesn't need.