The Observer Pattern: Telling Others Without Knowing Them

October 10, 2026 · 3 min read

When a library book is returned, several things should happen. The next person on the waiting list gets it held for them; they get an email; the catalogue shows it as available again. The tempting implementation is to do all of that in returnBook():

returnBook(copy: BookItem) {
  copy.status = 'available'
  this.holdQueue.assign(copy)
  this.mailer.sendHoldNotice(copy)
  this.searchIndex.update(copy)
}

Now the lending code imports the hold queue, the mailer and the search index. Adding late fines means editing it again. The Observer pattern reverses those dependencies:

A subject keeps a list of observers and notifies them when something happens. It knows only that they implement a small interface.

BookItem
HoldQueue
Mailer
SearchIndex
BookItem observers
HoldQueue SearchIndex

At startup, two services subscribe to the BookItem's "returned" event. BookItem stores them as a list of Observer interfaces — it doesn't import either class.

0 / 5

The code

interface ReturnObserver {
  onReturned(copy: BookItem): void
}

class BookItem {
  private observers: ReturnObserver[] = []
  status: 'on-loan' | 'available' | 'held' = 'on-loan'

  subscribe(o: ReturnObserver) {
    this.observers.push(o)
    return () => {
      this.observers = this.observers.filter((x) => x !== o)
    }
  }

  markReturned() {
    this.status = 'available'
    for (const o of [...this.observers]) o.onReturned(this)
  }
}

Two details matter. subscribe returns an unsubscribe function, so callers can clean up. And the loop iterates over a copy of the list, so an observer that unsubscribes while being notified doesn't make the loop skip the next one.

In practice you rarely subscribe to each object separately. More often, observers subscribe to a domain-wide event — "any copy returned" — through a shared emitter.

You already use it

  • The DOM: button.addEventListener('click', handler) — the button is the subject, handlers are observers. See event delegation.
  • Node's EventEmitter: emitter.on('data', fn).
  • React state stores: useSyncExternalStore(subscribe, getSnapshot) is the Observer contract — subscribe, get a callback, read the new value.
  • RxJS observables, MobX, Vue's reactivity: Observer with operators added.

The pitfalls

Errors. If one observer throws, the loop above stops, and later observers never hear the event. Decide on a policy. Usually that means catching each observer's error, reporting it, and carrying on:

for (const o of [...this.observers]) {
  try { o.onReturned(this) } catch (err) { reportError(err) }
}

Ordering. Observers run in subscription order, and nothing in the interface says so. If the mailer depends on the hold queue having run first, that's a hidden coupling. Make it explicit with a chained event instead: the hold queue publishes held, and the mailer listens to that.

Memory leaks. The subject holds a reference to every observer. A component that subscribes and never unsubscribes stays in memory for as long as the subject does — the classic leak in long-lived single-page apps. Always keep the unsubscribe function and call it on teardown.

Re-entrancy. An observer that changes the subject during notification can trigger another round of notifications before the first finishes. Keep handlers small, or queue events and deliver them after the current one completes.

Invisible control flow. "What happens when a book is returned?" no longer has an answer in one file. That's the cost of decoupling. Keep a list of subscribers somewhere discoverable, and log events in development.

Observer vs. pub/sub vs. message queues

ObserverIn-process pub/subMessage broker
Subscribers know the subject?Yes — they subscribe to itNo — only a topic nameNo
DeliverySynchronous callUsually synchronousAsynchronous, durable
Survives a crash?NoNoYes

The ideas are the same at every scale: the code that publishes doesn't know who listens. Once listeners live in other services and must not miss events, you need a broker — see message queues vs. event logs and the outbox pattern for publishing reliably.

In an interview

Observer is the expected answer whenever a requirement says "notify": reservation alerts in a library, price-drop alerts, seat-availability updates in ticket booking. Say what the event is, who subscribes, and whether delivery must be reliable — that last question is where a good answer turns into a system design discussion. Practise it on the library problem in the LLD practice tool.