ESC

Type to search the knowledge base.

Pub Sub vs Observer

Separate publishers from subscribers vs subject–observer coupling — when each fits UI apps, and a tiny EventEmitter sketch.

intermediate3 min read
  • javascript
  • pub-sub

Both patterns fan out “something happened” to multiple listeners. The difference is coupling and topology.

Observer

A subject keeps a list of observers and notifies them directly. Observers know the subject (or at least subscribe on it). Classic OOP textbook:

class Subject {
  constructor() {
    this.observers = new Set();
  }
  subscribe(fn) {
    this.observers.add(fn);
    return () => this.observers.delete(fn);
  }
  notify(data) {
    for (const fn of this.observers) fn(data);
  }
}

const weather = new Subject();
const unsub = weather.subscribe((t) => console.log('temp', t));
weather.notify(22);
unsub();

DOM events are observer-like: you call element.addEventListener on a known target.

Pub/Sub

Publishers emit named events on a broker (bus). Publishers don’t hold subscriber lists; subscribers don’t know who published. Coupling is to the channel name, not the instance.

function createBus() {
  const map = new Map(); // event -> Set<fn>

  return {
    on(type, fn) {
      if (!map.has(type)) map.set(type, new Set());
      map.get(type).add(fn);
      return () => map.get(type)?.delete(fn);
    },
    emit(type, payload) {
      map.get(type)?.forEach((fn) => fn(payload));
    },
    off(type, fn) {
      map.get(type)?.delete(fn);
    },
  };
}

const bus = createBus();
bus.on('cart:add', (item) => console.log(item));
bus.emit('cart:add', { id: 'sku_1' });

Comparison

Observer Pub/Sub
Knows peers? Subject knows observers Via channel only
Discovery Hold subject ref Agree on event names
Risk Tight object graphs Global bus spaghetti
UI fit Component state, DOM nodes Cross-feature app events

Real frontend mapping

  • React props/callbacks / context — structured data flow; not a free bus
  • Redux / Zustand subscriptions — store as subject
  • CustomEvent on window — pub/sub with DOM as broker (origin-wide noise)
  • Node EventEmitter — named events on an instance (hybrid)

Footguns shared by both

  1. Forgotten unsubscribe → leaks (especially SPA navigations)
  2. Sync notify re-entrancy — emit inside a handler that emits again
  3. Error in one subscriber killing the loop — isolate:
notify(data) {
  for (const fn of this.observers) {
    try { fn(data); }
    catch (e) { console.error(e); }
  }
}
  1. Event name sprawl — treat names as an API; version or namespace (checkout:paid)

When to pick which

  • Observer: lifecycle tied to one object (model, store, DOM node).
  • Pub/Sub: distant features must react without importing each other — then constrain the bus (modules own channels, not one god object).

Interview answer (out loud)

“Observer: subject holds observers and notifies them. Pub/sub: a broker routes named events so publishers and subscribers stay decoupled. DOM is observer-like; an app-wide event bus is pub/sub. Both need unsubscribe discipline to avoid SPA leaks.”

Once listeners

function once(bus, type, fn) {
  const off = bus.on(type, (payload) => {
    off();
    fn(payload);
  });
  return off;
}

Common for handshake events (ready). Built into Node’s EventEmitter as once.

Typing event maps (TypeScript sketch)

type Events = {
  'cart:add': { id: string };
  'cart:clear': void;
};
// bus.on/emit keyed by Events — prevents stringly-typed chaos

Treat the event map as a versioned public API of the module.

Further reading

Related guides