Observer Pattern

7 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Observer Pattern

1. What Is It?

The pattern defines a one-to-many dependency between objects so that when one object (the Subject) changes , all its dependents (Observers) are notified and updated automatically.

Without it, you either poll the subject for changes (wastes resources, adds ) or hard-code update calls directly into the subject (tight , violates SRP — the subject shouldn't know who cares about its ). decouples the thing that changes from the things that react to that change.


QUICK CHECK

A stock trading dashboard needs to update multiple UI widgets (a price chart, a portfolio summary, and an alerts panel) whenever a stock's price changes. A developer proposes directly calling each widget's update method from inside the stock price service whenever a new price arrives. What is the primary drawback of this approach compared to using the Observer pattern?

Choose one answer

2. How It Works

Participants:

  • Subject (also called Observable) — maintains a list of observers; provides attach(), detach(), and notify() operations
  • ConcreteSubject — stores ; calls notify() when its changes
  • Observer declaring the update() method
  • ConcreteObserver — implements update(); pulls or receives new state from the subject

Step-by-step:

  1. Define the Observer with update()
  2. Define the Subject with attach, detach, notify
  3. ConcreteSubject holds state; on each state change, calls notify() which loops over registered observers and calls their update()
  4. ConcreteObserver registers itself with the subject; on update(), reads new state and reacts

Push vs. Pull:

  • Push model: Subject sends state data directly in the update(event) call — observer doesn't need to query back
  • Pull model: Subject calls update() with no data; observer calls subject.getState() — looser but more roundtrips

Python:

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List


@dataclass
class StockEvent:
    ticker_symbol: str
    price: float


class StockObserver(ABC):
    @abstractmethod
    def update(self, event: StockEvent) -> None:
        pass


class StockTicker:
    def __init__(self, ticker_symbol: str) -> None:
        self._ticker_symbol = ticker_symbol
        self._price: float = 0.0
        self._observers: List[StockObserver] = []

    def attach(self, observer: StockObserver) -> None:
        self._observers.append(observer)

    def detach(self, observer: StockObserver) -> None:
        self._observers.remove(observer)

    def set_price(self, price: float) -> None:
        self._price = price
        self._notify()

    def _notify(self) -> None:
        event = StockEvent(self._ticker_symbol, self._price)
        for observer in self._observers:
            observer.update(event)


class PriceAlertObserver(StockObserver):
    def __init__(self, alert_threshold: float) -> None:
        self._alert_threshold = alert_threshold

    def update(self, event: StockEvent) -> None:
        if event.price > self._alert_threshold:
            print(f"ALERT: {event.ticker_symbol} hit ${event.price:.2f} "
                  f"(threshold: ${self._alert_threshold:.2f})")


class PriceDashboardObserver(StockObserver):
    def update(self, event: StockEvent) -> None:
        print(f"Dashboard updated: {event.ticker_symbol} = ${event.price:.2f}")


# Usage
ticker = StockTicker("AAPL")
ticker.attach(PriceDashboardObserver())
ticker.attach(PriceAlertObserver(alert_threshold=180.0))

ticker.set_price(175.0)
# Dashboard updated: AAPL = $175.00

ticker.set_price(185.0)
# Dashboard updated: AAPL = $185.00
# ALERT: AAPL hit $185.00 (threshold: $180.00)

Java:

import java.util.ArrayList;
import java.util.List;

// Event data (push model)
public record StockEvent(String tickerSymbol, double price) {}

// Observer interface
public interface StockObserver {
    void update(StockEvent event);
}

// Subject (ConcreteSubject)
public class StockTicker {
    private final String tickerSymbol;
    private double price;
    private final List<StockObserver> observers = new ArrayList<>();

    public StockTicker(String tickerSymbol) {
        this.tickerSymbol = tickerSymbol;
    }

    public void attach(StockObserver observer) {
        observers.add(observer);
    }

    public void detach(StockObserver observer) {
        observers.remove(observer);
    }

    public void setPrice(double price) {
        this.price = price;
        notifyObservers();
    }

    private void notifyObservers() {
        StockEvent event = new StockEvent(tickerSymbol, price);
        for (StockObserver observer : observers) {
            observer.update(event);
        }
    }
}

// ConcreteObserver A
public class PriceAlertObserver implements StockObserver {
    private final double alertThreshold;

    public PriceAlertObserver(double alertThreshold) {
        this.alertThreshold = alertThreshold;
    }

    @Override
    public void update(StockEvent event) {
        if (event.price() > alertThreshold) {
            System.out.printf("ALERT: %s hit $%.2f (threshold: $%.2f)%n",
                    event.tickerSymbol(), event.price(), alertThreshold);
        }
    }
}

// ConcreteObserver B
public class PriceDashboardObserver implements StockObserver {
    @Override
    public void update(StockEvent event) {
        System.out.printf("Dashboard updated: %s = $%.2f%n",
                event.tickerSymbol(), event.price());
    }
}

// Client
public class Main {
    public static void main(String[] args) {
        StockTicker ticker = new StockTicker("AAPL");
        ticker.attach(new PriceDashboardObserver());
        ticker.attach(new PriceAlertObserver(180.0));

        ticker.setPrice(175.0);  // Dashboard updated: AAPL = $175.00
        ticker.setPrice(185.0);  // Dashboard + Alert
    }
}

QUICK CHECK

A StockTicker subject notifies observers by calling update(StockEvent event), passing the latest price directly in the call. A junior developer suggests refactoring so that notify() calls update() with no arguments, and each observer instead calls ticker.getPrice() to retrieve the current value. What is the key trade-off of switching to this approach?

Choose one answer

3. Variants & Comparisons

VariantHow It WorksProsConsBest For
Push modelSubject sends event data in update(event)Observer needs no back-reference to subjectObservers receive data they may not needEvent-driven systems, decoupled observers
Pull modelSubject calls update() with no data; observer queries subjectObserver only fetches what it needsObserver must hold subject referenceObservers needing selective state
Event Bus / MediatorEvents published to a bus; observers subscribe by typeSubjects and observers never reference each otherExtra indirection, harder to traceLarge systems with many event types

Java: java.util.Observable (deprecated in Java 9+) and java.util. were the original GoF implementation. Modern Java prefers PropertyChangeListener (from java.beans) or reactive streams (RxJava, Project Reactor). Python: Built-in signal library or observable patterns via simple lists are common; __slots__ + property setters can trigger observers cleanly.


4. When to Use It (and When NOT To)

Use when:

  • Multiple components need to react to changes in another component (UI, event systems, stock feeds)
  • You want to avoid polling
  • The subject shouldn't know the concrete types of its observers

Don't use when:

  • The chain of notifications is complex — cascading updates become hard to trace (who triggered what?)
  • Observers are always the same set and won't change — just call them directly
  • Notification order matters and must be guaranteed — gives no ordering guarantees

Decision triggers:

  • "When X changes, Y and Z need to know about it, but X shouldn't know about Y or Z" →
  • "I keep adding new notify*() methods to a class every time a new consumer appears" → Observer

Anti-patterns:

  • Memory leaks: Forgetting to detach observers. A subject holding strong references to observers prevents GC. Use weak references or explicit lifecycle management.
  • Cascading updates: Observer A's update triggers Subject B, which notifies Observer C, which loops back — hard to debug, can cause infinite loops.

QUICK CHECK

A stock trading dashboard has a price feed component that updates frequently. Several UI widgets — a chart, a portfolio summary, and an alert banner — all need to refresh whenever the price changes. A developer considers using the Observer pattern. Which of the following scenarios would be the strongest reason to reconsider that choice?

Choose one answer

5. Real-World Usage

1. Java Swing event listeners: Every Swing widget is a Subject. button.addActionListener(listener) registers an . ActionListener.actionPerformed() is update(). Swing's entire event model is .

2. Android LiveData / ViewModel: LiveData<T> is the Subject. UI controllers call liveData.observe(lifecycleOwner, observer). When data changes, all active observers are notified. The lifecycle owner ensures automatic detach on destruction — solving the memory leak anti-pattern.

3. RxJava / Project Reactor: The entire reactive programming model is Observer formalized. Observable/Flux are subjects; Observer/Subscriber receive events. They add back-pressure, threading control, and operator pipelines on top of the core pattern.


QUICK CHECK

An Android app uses LiveData to expose user profile data from a ViewModel. A developer notices that in older implementations using raw Observer callbacks, Activity instances were sometimes kept alive in memory even after the user navigated away, because the Subject still held a reference to them. Which feature of Android LiveData directly addresses this memory leak problem?

Choose one answer

6. Interview Cheat Sheet

Key sentences:

  1. " decouples the thing that changes (Subject) from the things that react (Observers) — the subject only knows about the , not concrete implementations."
  2. "Push model sends data to observers; pull model lets observers query. Push is more decoupled; pull avoids sending irrelevant data."
  3. "The classic pitfall is memory leaks — if you don't detach observers, the subject holds references forever, preventing garbage collection."
  4. "In modern Java, PropertyChangeListener and reactive streams (Project Reactor, RxJava) are the production-grade evolution of this pattern."

Common follow-up questions:

Q: What's the difference between Observer and Event Bus? A: Observer requires observers to register directly on the subject. An Event Bus (Mediator variant) adds a central broker — subjects and observers never reference each other, at the cost of more indirection.

Q: How do you prevent memory leaks in Observer? A: Use weak references (Python weakref, Java WeakReference), enforce explicit detach() calls on lifecycle events, or use a framework that handles registration lifecycle (e.g., Android LiveData tied to a LifecycleOwner).

Q: What about thread safety? A: The basic pattern isn't thread-safe. If observers are added/removed concurrently, the observer list needs synchronization (e.g., CopyOnWriteArrayList in Java for safe iteration).

Connections:

  • Observer is the foundation of Model-View-Controller — Model is the Subject, Views are Observers
  • Observer + Mediator → Event Bus pattern
  • Observer is the imperative precursor to Reactive Streams (RxJava, Project Reactor)
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.