Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Observer

One-to-many notification without the sender knowing who is listening — and the leak it invites.


A thermometer reading changes. A display should update, a log should record it, and above 28° an alarm should fire.

What people write first

class Thermometer {
private temperature = 21;
set(value: number) {
this.temperature = value;
this.display.render(value); // ← now the thermometer
this.logger.write(value); // knows about three
if (value > 28) this.alarm.trigger(); // unrelated subsystems
}
}

Every new reaction means editing Thermometer. Worse, Thermometer now cannot be used anywhere those three classes do not exist — you cannot test it without constructing a logger, and you cannot reuse it in a context that has no display.

The pattern

Invert it. The thermometer keeps a list of things to call, and does not know what any of them are.

type Listener = (temperature: number) => void;
class Thermometer {
private listeners = new Set<Listener>();
private temperature = 21;
/** Returns the unsubscribe function — see the gotcha below. */
subscribe(listener: Listener): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
set(value: number) {
this.temperature = value;
for (const listener of this.listeners) listener(value);
}
}

Detach a subscriber and push the temperature around:

Subject

21°C

The subject has no idea who is listening — it calls notify() and stops caring.

  • waiting…
  • waiting…
  • detached

The three problems

Observer is one of the few catalogue patterns with genuine, well-known hazards.

Iterating a list that is being modified

An observer that unsubscribes itself while the loop is running will corrupt the iteration in most languages:

// Copy first, then notify.
for (const listener of this.listeners) listener(value);
for (const listener of [...this.listeners]) listener(value);

JavaScript’s Set iterator happens to tolerate deletion, but an observer that adds one during notification will see it called in the same round — probably not what anyone intended. Copying makes the behaviour explicit either way.

Where it already is

The DOM (addEventListener), Node’s EventEmitter, RxJS, Svelte stores, React’s useSyncExternalStore, every reactive framework’s dependency tracking, and every message broker. Observer is less a pattern you implement than one you recognise.

Which is a reason to use the platform’s version rather than writing your own: EventTarget is built in, handles the copy-before-iterate problem, and supports AbortSignal for bulk unsubscription.

const controller = new AbortController();
target.addEventListener('reading', onReading, {signal: controller.signal});
controller.abort(); // removes every listener registered with this signal

Push or pull

The version above pushes the new value into each observer. The alternative is to notify with nothing and let each observer pull what it needs:

for (const listener of this.listeners) listener(); // "something changed"

Push is simpler and avoids observers reading a subject that has changed again since. Pull scales better when the subject is large and different observers care about different parts of it — which is exactly the trade a fine-grained reactive system is making when it tracks dependencies per property.