All topics
Patternsintermediate

Observer Pattern and Pub/Sub

A pattern where one or more 'observers' subscribe to notifications from a 'subject,' decoupling the code that triggers events from the code that reacts to them.

One object notifies many others when something happens. The notifier does not know who is listening.

The Observer pattern is like a magazine subscription service: the publisher (subject) doesn't need to personally know each individual subscriber's reading habits or preferences — it just mails out the next issue to everyone currently on its subscriber list, and subscribers can join or cancel their subscription independently without the publisher needing to change anything about how it prints and distributes issues.

Key Concepts

1
    class EventEmitter {
      #listeners = new Map();
2
      on(event, fn) {
        (this.#listeners.get(event) ?? this.#listeners.set(event, []).get(event)).push(fn);
        return () => this.off(event, fn);       // an unsubscribe
      }
3
      off(event, fn) {
        const arr = this.#listeners.get(event) ?? [];
        const i = arr.indexOf(fn);
        if (i > -1) arr.splice(i, 1);
      }
4
      emit(event, data) {
        (this.#listeners.get(event) ?? []).slice().forEach(fn => fn(data));
      }
    }
5
slice() before iterating matters: a listener that unsubscribes during the emit would otherwise mutate the array being iterated, skipping the next one.
slice()
6
Observer against pub/sub, which are used interchangeably and differ slightly:
7
    observer   the subject holds the list; observers register
               with it directly
    pub/sub    a broker sits between. Publishers and subscribers
               do not know each other at all.
8
The second is more decoupled and harder to trace, because nothing names the connection.
9
Where you already use it. DOM events, Node's EventEmitter, WebSocket messages, RxJS, and every state library's subscribe method.
EventEmitter
10
The three problems it brings, which are worth stating.Leaks. A listener that is never removed keeps its closure — and anything it captured — alive. This is the most common memory leak in front-end code.
11
    const off = emitter.on('data', handler);
    off();                                    // in teardown
12
Returning the unsubscribe function from on makes cleanup hard to forget, which is why modern APIs do it.
on
13
Untraceable flow. "What happens when this fires" requires finding every subscriber, and nothing links them statically.
14
Ordering. Listeners run in registration order, which is usually incidental. Code that depends on it is fragile.
15
And error handling. One listener throwing should not stop the others:
16
    fn => { try { fn(data); } catch (e) { report(e); } }
17
Without that, a bug in one subscriber silently prevents every later one from running.
18
What the interviewer is probing.1. "What is the difference between observer and pub/sub?" Probing: coupling. Stalls: "They are the same." Moves up: in observer the subject knows its observers directly; in pub/sub a broker sits between, so publisher and subscriber never meet.
19
2. "How do these leak memory?" Probing: the lifecycle. Stalls: "They do not." Moves up: the subject holds a reference to each listener, so a component that never unsubscribes is never collected.
20
3. "How do you guard against one bad subscriber?" Probing: isolation. Stalls: "Trust them." Moves up: wrap each call in try/catch, or one throwing handler stops the rest from running.
21
4. "Where does this appear in the platform?" Probing: recognition. Stalls: "Nowhere." *Moves up:* addEventListener, EventTarget, and Node's EventEmitter are all this pattern.