StudyDSA logoStudyDSA

Command Palette

Search for a command to run...

Sign InSign Up
Sign Up

Data StructuresAlgorithmsBig-O NotationDesign PatternsSystem DesignMachine LearningPhysicsRoboticsAI Research

Factory MethodAbstract FactoryBuilderPrototypeSingletonAdapterBridgeCompositeDecoratorFacadeFlyweightProxyChain of ResponsibilityCommandIteratorMediatorMementoObserverStateStrategyTemplate MethodVisitor

Definition
StudyDSA

Where complexity meets clarity.
By Armas Zarra.

Topics

  • Data Structures
  • Algorithms
  • Big-O Notation
  • Robotics
  • AI Research
  • Machine Learning

Practice

  • Blind 75
  • LeetCode 75
  • NeetCode 150

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Armas Films LLC

Design PatternsObserver

Observer

A behavioral pattern where dependents subscribe to a subject that notifies them all on every change, replacing hand-maintained update calls with one write path.

Definition

A value changes, and three other things were supposed to react. Whether they did depends on whether the person writing that particular write site remembered all three. Observer is the Gang of Four behavioral pattern whose stated intent is to define a one-to-many dependency so that when one object changes state, all its dependents are notified automatically, which deletes the word remembered from the previous sentence.


An observer setup is really just a list of callbacks the subject runs after changing itself. Subscribing means getting added to the list, notification means the subject walks it, and the data gains a single write path with the reactions attached, instead of reactions copy-pasted behind every place the data changes.

The Update Everyone Must Remember

Here's the remembering version. A spreadsheet cell feeds a bar chart and a sum label, and two write paths exist: keyboard entry, written carefully, and a file import, written on a Friday. Find what the import path forgot before reading the final comment:

The import forgot the label, so the program ends with the chart showing 8 and the label still claiming Sum: 5, two widgets confidently disagreeing about one number. Nothing crashed, and nothing will: stale-view bugs don't throw, they just sit there being wrong until a user notices. Every new write path and every new dependent multiplies the spots where someone has to remember, and remembering doesn't scale.

Change Once, Notify All

The observer version moves the reactions onto the data. Cell keeps a subscriber list, attach() adds to it, and set_value() stores the value then walks the list calling each observer's update(). Both the chart and label subscribe once at startup. Before reading past the code, predict the final print after values 5 and 8 flow through:

It prints 8 Sum: 8, both dependents agreeing, because there's exactly one write path and it carries the notifications itself. The import-path bug from the previous section is now unwritable: a new write site calls set_value() like everyone else and gets every subscriber for free, and a new dependent is one attach() call instead of an edit to every write site in the program.

Who Does What

Four roles. The Subject interface offers attach() and detach(), and the ConcreteSubject (Cell) holds state plus the list, notifying on change. The Observer interface declares update(), and ConcreteObservers react, here by pulling whatever state they care about from the subject passed to them, the pull model that keeps subjects ignorant of their dependents' needs.


The pattern works like a magazine subscription: the publisher keeps the list, prints once, and mails everyone, instead of readers phoning the press daily to ask if anything's new. The analogy walks straight into the pattern's most famous bug. A reader who moves without canceling keeps receiving copies forever, and an observer that's logically dead but never detached keeps receiving updates forever, pinned in memory by the subject's own reference to it. That leak is common enough to have a name, and it gets its own section below.

Observer: Step-by-Step

Installing the pattern is four moves:

1. Keep the List on the Subject

The Cell owns its subscriber list and the attach() door onto it. Dependents register themselves, which inverts the old arrangement where write sites had to know every dependent by name.

2. Notify After the State Settles

set_value() stores first, loops second, so every observer reads a finished, consistent subject. Notifying mid-mutation hands observers a half-built state, and the bugs that causes never reproduce on demand.

3. Let Observers Pull What They Need

The subject passes itself, and each observer reads its own slice: the chart takes the value, the label formats it. This pull model keeps the subject ignorant of what dependents care about, which is the whole decoupling.

4. Make Leaving as Easy as Joining

detach() is the method nobody calls and everybody should, because a subscriber that never leaves is a memory leak with a famous name. Every subscription should come with its cancellation in hand.
Leaks, Order, and Cascades

The forgotten unsubscribe is called the lapsed listener problem, a top source of memory leaks in garbage-collected languages: the subject's strong reference keeps a dead observer alive as long as the subject lives, and a long-lived subject accumulates ghosts. The fixes are disciplined deregistration or weak references, and the platforms have been shipping both: addEventListener grew a once option and AbortSignal removal, and React's useSyncExternalStore requires every subscribe function to return its own cleanup.


The other hazards come straight from the JDK's obituary for its own implementation. Java 9 deprecated java.util.Observable with unusually specific complaints: the notification order is unspecified, and state changes don't correspond one-for-one with notifications. Both translate into practice. Observers that depend on running before each other break on the next refactor, and an observer that mutates other subjects inside update() triggers cascades of notifications firing notifications, the debugging of which is its own genre of suffering.

Observer, Pub/Sub, or Mediator

You might think Observer and pub/sub are two names for one pattern, and the wire shape is close. The difference is the middleman. An observer subject holds direct references to its subscribers and calls them synchronously, while pub/sub inserts a broker so publishers and subscribers never know each other exists, usually asynchronously and often across processes. Same idea at two distances: Observer is in-process and intimate, pub/sub is decoupled to the point of strangers passing messages through a post office.


The nearer sibling is Mediator, and the hub's intelligence settles it. A subject broadcasts to whoever signed up, with no opinions about routing, while a mediator owns interaction rules and decides who hears what. The DOM offers a neat two-for-one here: registering a click handler is Observer, and the event then climbing the ancestor tree afterward is Chain of Responsibility, two patterns cooperating in every browser click you've ever handled.

The Go and Rust Versions

Go skips the Observer interface entirely: a subscriber list is a slice of functions, attaching appends a closure, and notifying ranges over the slice, which is the whole pattern in three lines of the code tab. When concurrency enters, the idiomatic upgrade is channels, each subscriber holding a receive channel and the subject fanning out sends.


Rust's version bakes the lapsed-listener cure into the types. The code tab's subject holds Weak references, so observers live exactly as long as something else owns them, and the notify loop upgrades each weak handle, delivering to the living and silently dropping the dead. Unsubscribing by existing, or rather by ceasing to, which turns the pattern's most famous leak into a non-event.

Best Practices

The pattern is a list and a loop, and these five keep the loop from becoming folklore:


The modern APIs hand you the discipline: React's subscribe functions return their own cleanup, and the DOM offers once and AbortSignal. Hand-rolled subjects deserve the same shape, an attach() that returns the matching unsubscribe.

Check Yourself

Two questions before you go. First: the import path's forgotten update shipped a stale label, so what structurally changed in the observer version that makes forgetting impossible rather than merely unlikely? Second: Java's deprecation notice named two flaws in its own Observable. Name them, and the practice each one implies for subjects you write. Both answers are on this page.


Then go spot it in the wild, which means opening nearly anything. Every addEventListener call is this pattern, every state-management store notifying components is this pattern, and every webhook your backend registers is its cross-network cousin. Last, find a place in your codebase where changing one value requires remembering to refresh something else by hand. Count the write sites that remember correctly, find the one that doesn't, and you've found both a live bug and this pattern's next home.

the_manual_sync.py

class Cell:
    def __init__(self):
        self.value = 0

class BarChart:
    def __init__(self):
        self.height = 0

    def redraw(self, cell: Cell) -> None:
        self.height = cell.value

class SumLabel:
    def __init__(self):
        self.text = "Sum: 0"

    def recompute(self, cell: Cell) -> None:
        self.text = "Sum: " + str(cell.value)

def set_from_keyboard(cell: Cell, chart: BarChart, label: SumLabel, value: int) -> None:
    cell.value = value
    chart.redraw(cell)
    label.recompute(cell)  # remembered here

def set_from_import(cell: Cell, chart: BarChart, label: SumLabel, value: int) -> None:
    cell.value = value
    chart.redraw(cell)
    # forgot the label: this write site is now a bug

# Example usage
cell, chart, label = Cell(), BarChart(), SumLabel()
set_from_keyboard(cell, chart, label, 5)
set_from_import(cell, chart, label, 8)
print(chart.height, label.text)  # 8 Sum: 5  <- out of sync

observer.py

from abc import ABC, abstractmethod

class Observer(ABC):  # Observer: one update interface for every dependent
    @abstractmethod
    def update(self, cell: "Cell") -> None: ...

class Cell:  # Subject: owns the data AND the subscriber list
    def __init__(self):
        self.value = 0
        self._observers: list[Observer] = []

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

    def detach(self, observer: Observer) -> None:
        self._observers.remove(observer)  # the half people forget

    def set_value(self, value: int) -> None:
        self.value = value
        # One write path, n reactions, zero forgotten dependents
        for observer in list(self._observers):
            observer.update(self)

class BarChart(Observer):
    def __init__(self):
        self.height = 0

    def update(self, cell: "Cell") -> None:
        self.height = cell.value  # pull model: read what you need

class SumLabel(Observer):
    def __init__(self):
        self.text = "Sum: 0"

    def update(self, cell: "Cell") -> None:
        self.text = "Sum: " + str(cell.value)

# Example usage
cell = Cell()
chart = BarChart()
label = SumLabel()
cell.attach(chart)
cell.attach(label)

cell.set_value(5)
cell.set_value(8)
print(chart.height, label.text)  # 8 Sum: 8