04-behavioral-patterns

Observer Pattern: One-to-Many Notification

Let a subject notify every interested dependent automatically on state change — the foundation of event-driven programming, and the pattern behind every listener API you've used.

August 11, 2026
lldbehavioralobserverpub-subevent-drivenreactivenotifications

Observer Pattern

Define a one-to-many dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are notified and updated automatically. Observer is the foundational pattern of event-driven programming — every addEventListener, every @EventListener, every UI callback traces back to this shape.

The core insight: the subject shouldn't need to know what its dependents do with a state change, only that they need to be told about it.


1. The Problem: Polling

Without Observer, interested parties either poll the subject repeatedly (wasteful, laggy) or the subject hardcodes calls to every dependent it knows about (tightly coupled, and it must be edited every time a new dependent shows up).

java
// VIOLATION A: polling — wasteful and laggy
class StockTicker {
    private double price;
    double getPrice() { return price; }
    void setPrice(double price) { this.price = price; }
}
 
class MobileAppPoller {
    void poll(StockTicker ticker) {
        while (true) {
            double price = ticker.getPrice(); // busy-waits, wastes CPU, adds latency
            System.out.println("Price: " + price);
            try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
        }
    }
}
java
// VIOLATION B: subject hardcodes every dependent — OCP violation
class StockTicker {
    private MobileApp mobileApp;
    private EmailAlertService emailAlerts;
    private Dashboard dashboard;
 
    void setPrice(double price) {
        // Adding a new dependent (e.g. SmsAlertService) means editing this method
        mobileApp.refresh(price);
        emailAlerts.checkThreshold(price);
        dashboard.update(price);
    }
}

Neither approach scales: polling wastes resources and adds latency; hardcoding is a growing, tightly-coupled dependency list living inside the subject.


2. Structure

The subject holds a collection of observers behind the Observer interface — it never depends on a concrete MobileAppObserver or EmailAlertObserver. Adding SmsAlertObserver requires zero changes to StockTicker: it just calls attach().


3. Full Implementation

java
// The Observer contract — anything that wants to be notified implements this
interface Observer {
    void update(double price);
}
 
// The Subject contract — anything that broadcasts state changes implements this
interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}
 
class StockTicker implements Subject {
    private final List<Observer> observers = new ArrayList<>();
    private double price;
 
    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }
 
    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }
 
    @Override
    public void notifyObservers() {
        // push notification: subject sends the new state directly
        for (Observer observer : observers) {
            observer.update(price);
        }
    }
 
    void setPrice(double price) {
        this.price = price;
        notifyObservers(); // state change triggers notification automatically
    }
}
 
class MobileAppObserver implements Observer {
    private final String userId;
    MobileAppObserver(String userId) { this.userId = userId; }
 
    @Override
    public void update(double price) {
        System.out.printf("[App:%s] Price updated to $%.2f%n", userId, price);
    }
}
 
class EmailAlertObserver implements Observer {
    private final double threshold;
    EmailAlertObserver(double threshold) { this.threshold = threshold; }
 
    @Override
    public void update(double price) {
        if (price > threshold) {
            System.out.printf("[Email] ALERT: price $%.2f crossed threshold $%.2f%n", price, threshold);
        }
    }
}
 
class Demo {
    public static void main(String[] args) {
        StockTicker ticker = new StockTicker();
 
        Observer app = new MobileAppObserver("u123");
        Observer emailAlert = new EmailAlertObserver(150.0);
 
        ticker.attach(app);
        ticker.attach(emailAlert);
 
        ticker.setPrice(148.50); // both notified; only email alert fires its condition
        ticker.setPrice(151.20); // both notified; email alert now triggers
 
        ticker.detach(emailAlert); // stop receiving future updates
        ticker.setPrice(160.00);   // only app is notified now
    }
}

4. Runtime Sequence

StockTicker never branches on which kind of observer it's calling — the loop in notifyObservers() is identical regardless of how many, or which types of, observers are attached.


5. Push vs. Pull Notification

Two ways for update() to hand data to the observer, each with a real trade-off:

java
// PUSH: subject sends the full changed state directly (what we did above)
interface PushObserver {
    void update(double price); // subject decides what data to send
}
 
// PULL: subject only signals "something changed"; observer queries what it needs
interface PullObserver {
    void update(Subject subject); // observer calls back into subject for details
}
 
class DashboardPullObserver implements PullObserver {
    @Override
    public void update(Subject subject) {
        StockTicker ticker = (StockTicker) subject;
        // observer pulls exactly the fields it needs, ignoring the rest
        System.out.println("Current price: " + ticker.getPrice());
    }
}
PushPull
CouplingSubject must know what data every observer needsObserver decides what it needs — subject stays generic
BandwidthSends the same payload to every observer, even unused fieldsEach observer fetches only what it needs
SimplicitySimpler for a single, well-known data shapeNeeds a getter surface on the subject; slightly more ceremony
Scales whenThe notified data is small and uniform (a price)Observers have widely varying data needs

Most real listener APIs (DOM events, Java's PropertyChangeListener) pass an event object rather than raw pushed values — a hybrid: push a small envelope, let the observer pull specifics off it if needed.


6. A Second Worked Example: Generic, Typed Event Bus

The stock ticker example is single-purpose — one subject, one event type. A more general, reusable Observer implementation supports multiple, independently-typed events on the same subject, closer to what a real domain object needs:

java
interface DomainEvent {}
 
record OrderPlaced(String orderId, double amount) implements DomainEvent {}
record OrderCancelled(String orderId, String reason) implements DomainEvent {}
 
interface EventListener<T extends DomainEvent> {
    void onEvent(T event);
}
 
class Order {
    private final Map<Class<?>, List<EventListener<?>>> listeners = new HashMap<>();
    private String status = "PENDING";
 
    <T extends DomainEvent> void on(Class<T> eventType, EventListener<T> listener) {
        listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
    }
 
    @SuppressWarnings("unchecked")
    private <T extends DomainEvent> void emit(T event) {
        List<EventListener<?>> forType = listeners.getOrDefault(event.getClass(), List.of());
        for (EventListener<?> listener : forType) {
            ((EventListener<T>) listener).onEvent(event);
        }
    }
 
    void place(String orderId, double amount) {
        status = "PLACED";
        emit(new OrderPlaced(orderId, amount));
    }
 
    void cancel(String orderId, String reason) {
        status = "CANCELLED";
        emit(new OrderCancelled(orderId, reason));
    }
}
 
class Demo2 {
    public static void main(String[] args) {
        Order order = new Order();
 
        order.on(OrderPlaced.class, e ->
            System.out.println("[Inventory] reserve stock for " + e.orderId()));
        order.on(OrderCancelled.class, e ->
            System.out.println("[Inventory] release stock, reason: " + e.reason()));
 
        order.place("ORD-1", 99.0);
        order.cancel("ORD-1", "customer request");
    }
}

This is still Observer — Order never knows what a listener does with an event — but it generalizes to multiple event types per subject and typed payloads, which is closer to how domain event patterns work in real applications (and one step away from a full Pub-Sub broker, covered next).


7. The Memory Leak Trap

The most common production bug with Observer: forgetting to detach. If an observer's lifecycle is shorter than the subject's, and it never unsubscribes, the subject holds a strong reference to it forever — the observer (and everything it references) cannot be garbage collected.

java
// VIOLATION: a short-lived UI component attaches but never detaches
class OrderDetailsScreen implements Observer {
    OrderDetailsScreen(StockTicker ticker) {
        ticker.attach(this); // subscribed...
    }
    // ...screen is closed/destroyed, but never called ticker.detach(this).
    // StockTicker (long-lived, e.g. a singleton service) now holds a reference
    // to a "dead" screen forever — classic listener leak.
 
    @Override
    public void update(double price) { /* update UI */ }
}
🚨

Fix: pair every attach() with a guaranteed detach() — in a close()/dispose() method, a try-with-resources block, or a framework lifecycle hook (@PreDestroy, Android's onDestroy()). For cases where explicit cleanup is unreliable, use WeakReference-backed observer lists so the subject doesn't keep dead observers alive — at the cost of non-deterministic cleanup timing.

A WeakReference-backed list looks like this:

java
class WeakObserverList implements Subject {
    private final List<WeakReference<Observer>> observers = new ArrayList<>();
    private double price;
 
    @Override
    public void attach(Observer observer) {
        observers.add(new WeakReference<>(observer));
    }
 
    @Override
    public void detach(Observer observer) {
        observers.removeIf(ref -> ref.get() == observer || ref.get() == null);
    }
 
    @Override
    public void notifyObservers() {
        // Cleans up garbage-collected observers as it goes — no explicit detach() required
        observers.removeIf(ref -> ref.get() == null);
        for (WeakReference<Observer> ref : observers) {
            Observer observer = ref.get();
            if (observer != null) observer.update(price);
        }
    }
}

8. Thread Safety: Concurrent Modification During Notification

A second, subtler production bug: an observer's update() method calling back into attach() or detach() (directly, or via a UI event triggered by the notification) while notifyObservers() is still iterating — a ConcurrentModificationException waiting to happen.

java
// VIOLATION: detaching during notification throws ConcurrentModificationException
class Broken {
    void notifyObservers(List<Observer> observers, double price) {
        for (Observer o : observers) {          // iterating...
            o.update(price);                     // ...and update() calls observers.remove(this)
        }
    }
}
java
// FIXED: snapshot the list before iterating, or use a thread-safe, copy-on-write collection
class StockTicker implements Subject {
    private final List<Observer> observers = new CopyOnWriteArrayList<>();
    private double price;
 
    @Override
    public void notifyObservers() {
        // CopyOnWriteArrayList's iterator works off a stable snapshot —
        // safe even if an observer detaches itself mid-notification
        for (Observer observer : observers) {
            observer.update(price);
        }
    }
    // attach/detach omitted — same as before
}

CopyOnWriteArrayList is the standard, idiomatic choice for observer lists that are notified far more often than they're mutated (the typical case: many notifyObservers() calls, occasional attach/detach). For attach/detach-heavy workloads, snapshot the list into an array immediately before iterating instead.


9. When to Use vs. When It's Overkill

Use Observer whenSkip it when
Multiple, independent parts of the system need to react to one object's state changesOnly one dependent ever cares — a direct method call is simpler and more traceable
The set of interested parties is dynamic (added/removed at runtime)The set of dependents is fixed and small, known entirely at compile time
Subject and observers should not know each other's concrete typesExtremely simple in-process code where an interface adds no value
You're implementing UI event handling, listeners, or a domain "on state changed" hookThe chain of who-notifies-whom is more than one hop deep — consider Pub-Sub instead
⚠️

Over-applying Observer: using it for a single, always-present, tightly coupled dependency (there is exactly one consumer of this event, forever) just adds an interface and a collection for no real decoupling benefit. A direct method call is the right level of ceremony there.


10. Observer vs. Pub-Sub

Observer and Pub-Sub solve the same underlying need — one-to-many notification — but differ in whether the subject and the listeners know about each other directly:

ObserverPub-Sub
CouplingSubject holds direct references to observersPublisher and subscriber never reference each other — a broker mediates
LocationTypically in-process, same address spaceOften distributed — broker can be a separate process/service
Subject's jobManage its own list of observers, call them directlyPublish to a named channel/topic; the broker fans out
Failure isolationAn observer throwing can affect the subject's notifyObservers() loopBroker typically isolates publisher from subscriber failures

Pub-Sub is Observer generalized with an explicit event channel in between. See Publish-Subscribe Pattern for the full broker-mediated version and when the extra indirection is worth it.


11. Real-World / Production Examples

DOM eventselement.addEventListener('click', handler) is Observer: the DOM element is the subject, handler is the observer.

Java PropertyChangeListener — the classic JavaBeans Observer implementation, still used in Swing and some enterprise codebases:

java
class TemperatureSensor {
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    private double temperature;
 
    void addListener(PropertyChangeListener listener) { support.addPropertyChangeListener(listener); }
 
    void setTemperature(double newTemp) {
        double old = this.temperature;
        this.temperature = newTemp;
        support.firePropertyChange("temperature", old, newTemp); // notifies all listeners
    }
}

Spring's ApplicationEventPublisher / @EventListener — Spring beans publish domain events (OrderPlacedEvent); any bean with a matching @EventListener method is notified, decoupled via the ApplicationContext:

java
@Component
class InventoryEventHandler {
    @EventListener
    void onOrderPlaced(OrderPlacedEvent event) {
        System.out.println("Reserving stock for " + event.getOrderId());
    }
}
// Somewhere else: applicationEventPublisher.publishEvent(new OrderPlacedEvent(orderId));

Reactive programming (RxJava, Project Reactor)Observable/Flux streams are Observer at scale, adding operators (map, filter, debounce) on top of the same subscribe/notify shape.

Git hooks / CI webhooks — a push to a repo (subject state change) notifies every configured webhook (observer) — a distributed variant closer to Pub-Sub.

Stock ticker / live dashboards — exactly the example above: price changes broadcast to every subscribed display, alert service, and logging pipeline.


Interview Questions

  • Walk through the attach/detach/notify contract. What does the subject need to know about its observers, and what does it not need to know?
  • What's the difference between push and pull notification models? Give a scenario where pull is clearly better.
  • How does forgetting to call detach() cause a memory leak? How would you defend against it in a long-lived subject?
  • Why does iterating a plain ArrayList of observers risk a ConcurrentModificationException, and how does CopyOnWriteArrayList fix it?
  • Compare Observer and Pub-Sub. What's the concrete difference in how a publisher/subject reaches its listeners?
  • How does Spring's @EventListener mechanism relate to the Observer pattern? What plays the role of subject, observer, and (if any) broker?
  • If an observer's update() method throws an exception, what should happen to the remaining observers in the notification loop? How would you design for that?
  • Why is Observer described as "the foundation of event-driven programming"? What other patterns/paradigms build on it?
  • When would you choose a direct method call over introducing an Observer relationship?

Quick Reference

QuestionAnswer
What does Observer notify on?A state change in the subject
What's the extension point?A new class implementing Observer
Coupling modelSubject holds direct references to observers
What GoF category?Behavioral
Closest sibling patternPub-Sub (adds a broker in between)
Typical JDK examplePropertyChangeListener, DOM addEventListener