Facebook Pixel

Observer Pattern

Reacting to change without tight coupling

A stock ticker publishes a new price. A price display must refresh. An alert engine must check whether the price crossed a threshold. An analytics logger must record the tick. All three need to react to the same event, but none of them should be wired directly into the ticker. If the ticker calls each one by name, adding a fourth consumer means editing the ticker's source code — and the ticker should know nothing about displays, alerts, or analytics.

The naive version makes this coupling explicit and fragile:

The Observer pattern decouples the ticker from its consumers via a registration list.

1# Bad: the ticker is hard-coded to three specific consumers.
2# Adding an audit log means editing StockTicker.
3class StockTicker:
4    def __init__(self) -> None:
5        self._display = PriceDisplay()
6        self._alert = AlertEngine(threshold=150.0)
7        self._analytics = AnalyticsLogger()
8
9    def update_price(self, symbol: str, price: float) -> None:
10        self._price = price
11        self._display.refresh(symbol, price)       # tight coupling
12        self._alert.check(symbol, price)           # tight coupling
13        self._analytics.record(symbol, price)      # tight coupling

How it works

The Observer pattern separates the publisher (the subject) from its consumers (the observers) through a registration interface. The subject holds a list of observers. When its state changes, it calls notify() on each one. New consumers register themselves at startup; the subject never names them directly.


Adding a fourth consumer requires only a registration call at startup; the subject's source code does not change. This structure suits any case where one upstream event must fan out to multiple independent downstream reactions.

Good: publish via a registration list

1from __future__ import annotations
2from abc import ABC, abstractmethod
3
4class Observer(ABC):
5    @abstractmethod
6    def on_price_update(self, symbol: str, price: float) -> None: ...
7
8class StockTicker:
9    def __init__(self) -> None:
10        self._observers: list[Observer] = []
11        self._prices: dict[str, float] = {}
12
13    def subscribe(self, observer: Observer) -> None:
14        self._observers.append(observer)
15
16    def unsubscribe(self, observer: Observer) -> None:
17        self._observers.remove(observer)
18
19    def update_price(self, symbol: str, price: float) -> None:
20        self._prices[symbol] = price
21        self._notify(symbol, price)
22
23    def _notify(self, symbol: str, price: float) -> None:
24        for observer in list(self._observers):  # snapshot avoids mutation during iteration
25            observer.on_price_update(symbol, price)
26
27class PriceDisplay(Observer):
28    def on_price_update(self, symbol: str, price: float) -> None:
29        print(f"[DISPLAY] {symbol}: ${price:.2f}")
30
31class AlertEngine(Observer):
32    def __init__(self, threshold: float) -> None:
33        self._threshold = threshold
34
35    def on_price_update(self, symbol: str, price: float) -> None:
36        if price > self._threshold:
37            print(f"[ALERT] {symbol} crossed ${self._threshold:.2f} — now ${price:.2f}")
38
39class AnalyticsLogger(Observer):
40    def on_price_update(self, symbol: str, price: float) -> None:
41        print(f"[LOG] tick {symbol}={price}")
42
43# Wire up at startup; the ticker knows none of these classes by name.
44ticker = StockTicker()
45ticker.subscribe(PriceDisplay())
46ticker.subscribe(AlertEngine(threshold=150.0))
47ticker.subscribe(AnalyticsLogger())
48
49ticker.update_price("AAPL", 145.30)
50ticker.update_price("AAPL", 152.10)
51# [DISPLAY] AAPL: $145.30
52# [LOG] tick AAPL=145.3
53# [DISPLAY] AAPL: $152.10
54# [ALERT] AAPL crossed $150.00 — now $152.10
55# [LOG] tick AAPL=152.1

The notify fan-out as a sequence

It helps to trace what happens when a price update arrives:


Each observer runs synchronously in this implementation. In a production system you would push to a queue and let each consumer process asynchronously, so a slow logger cannot stall a display refresh. The pattern is the same either way; only the delivery mechanism changes.

Push vs. pull notification

When the subject notifies its observers, it can deliver the changed data directly — this is the push model. In the stock ticker example above, on_price_update(symbol, price) pushes both pieces of state to every observer. Push is straightforward and requires no additional calls, but it means every observer receives data whether or not it needs it. An observer that cares only about the symbol still receives the price on every tick.

The pull model inverts this. The subject sends a minimal notification — often just a reference to itself or a change token — and each observer queries the subject for only the state it requires. For the ticker, that means exposing a getter such as ticker.get_price(symbol) and notifying with just the changed symbol; an observer that tracks one symbol reads only that price. This avoids sending data that observers will discard, and it lets different observers fetch different fields. The trade-off is the extra call: each observer must reach back into the subject, which re-introduces a dependency on the subject's interface.

Push suits cases where observers uniformly need the same data and the payload is small. Pull suits cases where different observers need different slices of a large subject, or where observers need to fetch related state that cannot be anticipated at notification time.

Lapsed listeners and memory leaks

The most common production bug in Observer implementations is the lapsed listener: an observer registers with a subject but never unsubscribes. The subject holds a reference to the observer in its list. As long as that reference exists, the garbage collector cannot reclaim the observer or any objects the observer itself references. In a long-running process this silently accumulates memory.

The fix is explicit unsubscription: call unsubscribe(observer) when the observer's work is done, or when the component that owns it is destroyed. UI frameworks that use Observer internally (event listeners, component lifecycles) typically expose a teardown hook — onDestroy, componentWillUnmount, Disposable — precisely for this reason.

An alternative is to store observers as weak references. The subject's list holds a weak reference to each observer; if the observer has been garbage-collected, the notification loop skips the dead entry and removes it from the list. This avoids requiring callers to unsubscribe explicitly, but it means an observer may be silently dropped if nothing else holds a strong reference to it — a subtle lifetime bug in its own right. Explicit unsubscription is more predictable and the more common choice in production code.

When NOT to use this

Observer is the right tool when one upstream event fans out to multiple independent consumers and you want to add or remove consumers without touching the publisher. When there is exactly one consumer and no expectation of more, a direct method call is cleaner — every layer of indirection has a cost in readability and debuggability.

Circular subscription chains are a real hazard: observer A updates B which updates A. The infinite loop is silent until it overflows the stack. If your observers write back to the same subject they listen to, audit the update path carefully or make the subject ignore re-entrant notifications. In a multithreaded context, the snapshot trick in the _notify method above (iterating a copy of the list) prevents a ConcurrentModificationException when one observer unsubscribes another, but you still need a lock around the registration list itself.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro