Skip to main content
Low-Level Design · lesson 16 of 18
Open the Low-Level Design page →
Low-Level Design advanced Lesson 16 of 18

The Observer Pattern in Practice: A Stock Ticker Notifying Multiple Displays

AlertDisplay is fully coded but never called — the naive ticker forgot to wire it in. The Observer pattern makes that omission structurally impossible.

By SkillByExample · Updated

lld oop python design-patterns observer

Two displays work fine hardcoded by name. The third one is where the naive version breaks.

The naive version: hardcoded display calls

class PriceDisplay:
    def show(self, price):
        print(f"[PriceDisplay] Current price: {price}")

class ChartDisplay:
    def show(self, price):
        print(f"[ChartDisplay] Plotting point: {price}")

class AlertDisplay:
    def show(self, price):
        if price > 100:
            print(f"[AlertDisplay] ALERT: price {price} exceeds 100")

class NaiveStockTicker:
    def __init__(self):
        self.price_display = PriceDisplay()
        self.chart_display = ChartDisplay()
        # AlertDisplay exists but nobody wired it in

    def set_price(self, price):
        self.price_display.show(price)
        self.chart_display.show(price)

ticker = NaiveStockTicker()
ticker.set_price(101.5)
$ python naive_ticker.py
[PriceDisplay] Current price: 101.5
[ChartDisplay] Plotting point: 101.5

AlertDisplay is fully implemented and would correctly fire on a price over 100 — but NaiveStockTicker.__init__ never constructs one, and set_price never calls it. This is the same silent-omission bug from the Open/Closed lesson earlier in this track, wearing a different domain: the ticker has to be told about every display class by name, and one got missed.

The fix: the Observer pattern

class StockTicker:
    def __init__(self):
        self._observers = []

    def subscribe(self, observer):
        self._observers.append(observer)

    def set_price(self, price):
        for observer in self._observers:
            observer.update(price)

class PriceDisplay:
    def update(self, price):
        print(f"[PriceDisplay] Current price: {price}")

class ChartDisplay:
    def update(self, price):
        print(f"[ChartDisplay] Plotting point: {price}")

class AlertDisplay:
    def update(self, price):
        if price > 100:
            print(f"[AlertDisplay] ALERT: price {price} exceeds 100")

ticker = StockTicker()
ticker.subscribe(PriceDisplay())
ticker.subscribe(ChartDisplay())
ticker.subscribe(AlertDisplay())
ticker.set_price(101.5)
$ python observer_ticker.py
[PriceDisplay] Current price: 101.5
[ChartDisplay] Plotting point: 101.5
[AlertDisplay] ALERT: price 101.5 exceeds 100

Same three displays, same price — but this time AlertDisplay fires, because being included is just a matter of calling ticker.subscribe(AlertDisplay()) once. StockTicker itself never changed at all between the two-display and three-display versions.

Practice

1. Add an unsubscribe method and confirm it stops future notifications.
class StockTicker:
    def __init__(self):
        self._observers = []
    def subscribe(self, observer):
        self._observers.append(observer)
    def unsubscribe(self, observer):
        self._observers.remove(observer)
    def set_price(self, price):
        for observer in self._observers:
            observer.update(price)

class PriceDisplay:
    def update(self, price):
        print(f"[PriceDisplay] Current price: {price}")

ticker = StockTicker()
display = PriceDisplay()
ticker.subscribe(display)
ticker.set_price(100)
ticker.unsubscribe(display)
ticker.set_price(200)
print("done")
$ python unsubscribe.py
[PriceDisplay] Current price: 100
done

The second set_price(200) call produces no display output at all — _observers is empty after unsubscribe, so the loop inside set_price has nothing to iterate over.

2. Confirm subscribing the same observer twice causes duplicate notifications.
class StockTicker:
    def __init__(self):
        self._observers = []
    def subscribe(self, observer):
        self._observers.append(observer)
    def set_price(self, price):
        for observer in self._observers:
            observer.update(price)

class PriceDisplay:
    def update(self, price):
        print(f"[PriceDisplay] Current price: {price}")

ticker = StockTicker()
display = PriceDisplay()
ticker.subscribe(display)
ticker.subscribe(display)
ticker.set_price(100)
$ python double_subscribe.py
[PriceDisplay] Current price: 100
[PriceDisplay] Current price: 100

_observers is a plain list, so the same object can be appended twice — this is a real bug a production subscribe() should guard against, most simply by checking if observer not in self._observers before appending.

3. Add a brand-new observer type, confirming StockTicker needs zero changes.
class StockTicker:
    def __init__(self):
        self._observers = []
    def subscribe(self, observer):
        self._observers.append(observer)
    def set_price(self, price):
        for observer in self._observers:
            observer.update(price)

class LoggingDisplay:
    def update(self, price):
        print(f"[LoggingDisplay] logged price={price}")

ticker = StockTicker()
ticker.subscribe(LoggingDisplay())
ticker.set_price(150)
$ python new_observer.py
[LoggingDisplay] logged price=150

LoggingDisplay was never seen by StockTicker before this script — it works purely because it implements update(price), which is the entire contract subscribe/set_price depend on.

The next lesson, Design Patterns That Are Just Language Features, shows Strategy, Command and Iterator collapsing into functions, closures and generators.

Frequently Asked Questions

Isn't this the same problem as the Open/Closed Principle lesson earlier in this track?
Same underlying failure shape (a class hardcodes knowledge of specific other classes, and forgetting to update it silently drops behavior), different concrete pattern for fixing it. The shapes lesson fixed it with polymorphism (`shape.area()`); this lesson fixes the equivalent problem — a subject that needs to notify an open-ended set of listeners — with the Observer pattern specifically, which is the named, standard solution to exactly this shape of problem.
Why is design-patterns not its own topic on this site if this lesson uses one?
The standalone design-patterns topic (teaching all 23 Gang-of-Four patterns as a catalog) was evaluated and skipped — Refactoring Guru and tutorialspoint already do that well, including runnable examples. This lesson isn't a patterns catalog; it's one LLD problem (notify multiple displays without tight coupling) that happens to be best solved by naming and applying a specific pattern, the same way a real engineer would reach for it.
What's the actual method name difference between the naive and observer versions?
The naive version calls `.show(price)` on two specific, named attributes (`self.price_display`, `self.chart_display`). The observer version calls `.update(price)` on every item in a list (`self._observers`) without knowing or caring what type each one is — `StockTicker` only needs every observer to implement `update`, which is the contract that makes the list interchangeable.
Does subscribing the same observer object twice cause a bug?
Yes, and Practice #2 demonstrates it directly: it appears twice in `_observers`, so it receives every price update twice. A production implementation would typically use a `set` instead of a `list` (if observers are hashable) or check for existing membership in `subscribe()` before appending, to guard against this.