Beginner13 min readDesign Patternslive prototype

Observer

Let many dependents subscribe to one subject and get notified automatically the moment its state changes. Think of a YouTube channel: you hit πŸ”” Subscribe, and when the channel uploads, *every current subscriber* is pinged β€” the channel doesn't know who you are, it just walks its list. Unsubscribe and the pings stop. Nobody polls "is there a new video yet?" β€” the subject *pushes*.

The idea

What it is

Observer solves a problem every interactive program has: one object changes, and a bunch of other objects need to react. Its intent in one sentence: let many dependents (observers) subscribe to one subject, and have the subject notify all of them automatically whenever its state changes. The subject keeps a list of who's interested; when something happens, it walks that list and calls each one. It never hard-codes who is listening β€” observers add and remove themselves at runtime.

The perfect mental model is a YouTube channel. You hit the πŸ”” Subscribe button β€” that's you adding yourself to the channel's subscriber list. When the channel uploads a video, YouTube notifies every current subscriber, automatically. The channel doesn't know your name, doesn't care what device you're on, and never checks in with you individually β€” it just walks its list. Hit Unsubscribe and the notifications stop, because you're off the list. And crucially, you never have to refresh the channel page every minute asking "is there a new video yet?" β€” the news comes to you, exactly when it happens.

The one sentence to remember

Observer defines a one-to-many dependency: one subject, many observers. Observers subscribe() to the subject; when the subject's state changes it calls notify(), which walks the list and calls each observer's update(...) β€” the subject only ever knows the tiny Observer interface, never the concrete classes behind it.

You've already used it today

Every button.addEventListener("click", handler) you've ever written is the Observer pattern β€” the button is the subject, your handler is an observer on its list, and the click is the state change that triggers notify(). React re-rendering the UI when state changes, RxJS streams, and spreadsheet cells recalculating when another cell changes are all the same idea.

Mechanics

How it works

The two roles

Observer has exactly two participants, connected by one tiny interface:

  • Subject (the channel) β€” owns the state everyone cares about and a private list of observers. It exposes three doors: subscribe(observer) adds to the list, unsubscribe(observer) removes from it, and notify() β€” usually called internally right after state changes β€” walks the list and calls each observer.
  • Observer (the subscriber) β€” anyone who implements one small interface with a single method: update(...). That method is literally the subject calling you back. Concrete observers (a viewer, a UI panel, a logger) each do their own thing inside it.
  • The one-to-many trick β€” the subject stores observers by interface only. It can notify a viewer, a chart, and an email sender without knowing any of those classes exist. New observer types plug in without a single line changing in the subject.
typescript
interface Subscriber {
  update(video: string): void;          // the channel calls THIS
}

class Channel {
  private subscribers: Subscriber[] = [];   // the one-to-many list

  subscribe(s: Subscriber)   { this.subscribers.push(s); }               // πŸ””
  unsubscribe(s: Subscriber) { this.subscribers = this.subscribers.filter(x => x !== s); } // πŸ”•

  upload(title: string) {
    // 1. the subject's state changes...
    for (const s of this.subscribers) {  // 2. ...so it walks its list
      s.update(title);                   // 3. and pushes the news to each one
    }
  }
}

That for loop inside upload() is the entire engine. The channel never asks who is on the list or what they'll do with the news β€” it fires update() at each entry and moves on. Subscribe four viewers and the loop runs four times; unsubscribe three of them and the very next upload notifies exactly one. The set of listeners is a runtime decision, not a compile-time one.

No more polling

The alternative to Observer is polling: every dependent repeatedly asks the subject "changed yet? changed yet?" on a timer. That's how you'd check a channel with no subscribe button β€” reload the page every minute. Polling wastes work when nothing changed and reacts late when something did (up to one full interval late). Observer inverts the direction: dependents register once, then the subject pushes at the exact moment of change. Zero wasted checks, zero built-in lag.

Push vs pull: what does update() carry?

  • Push style β€” the subject sends the data along: update(video). The observer gets everything it needs in the call. Simple and fast, but the subject has to guess what every observer wants, and may end up shipping data most of them ignore.
  • Pull style β€” the subject sends only "something changed", typically passing itself: update(this). Each observer then reads exactly the state it cares about off the subject. More flexible when observers need different slices of state, at the cost of an extra round trip and a tighter grip on the subject.

The lapsed listener β€” Observer's classic memory leak

The subject's list holds a reference to every observer. Forget to unsubscribe() when an observer dies (a closed window, an unmounted component) and two bad things happen: the garbage collector can't reclaim it (the subject still points at it), and it keeps receiving updates as a ghost, often crashing on state it no longer has. Always pair every subscribe with an unsubscribe in teardown β€” component unmount, destructor, removeEventListener.

Order and reentrancy

The pattern makes no promise about notification order β€” don't write observers that only work if they run before some other observer. And be careful with reentrancy: an update() that mutates the subject or subscribes/unsubscribes mid-notification can corrupt the very list being walked (many implementations iterate over a copy for exactly this reason). Keep update() handlers small and side-effect-light.

Where you meet it in the wild

  • DOM events β€” addEventListener / removeEventListener are subscribe/unsubscribe on a subject (the element).
  • React and friends β€” state changes notify the framework, which re-renders the dependent UI. Modelβ†’view data binding in MVC/MVVM is the original GoF motivation.
  • RxJS / reactive streams β€” Observables generalise the pattern into composable event pipelines.
  • Webhooks β€” the same idea across the network: you register a URL with a service (subscribe), it POSTs to you when something happens (notify).

Observer vs pub/sub β€” close cousins, not twins

In Observer, the subject holds the list itself and calls its observers directly β€” the two sides share at least the update() interface and the observer knows which subject it signed up with. Pub/sub adds a broker in the middle (an event bus or message queue): publishers emit to the broker, subscribers register with the broker, and the two sides never know each other exists β€” not even by interface β€” and can even live in different processes. If you find yourself building a middleman object whose whole job is deciding who talks to whom, you're heading toward [[mediator]].

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation β€” no setup, no install. Play with it as you read.

About this simulation

A YouTube channel with four viewers. Hit πŸ”” Subscribe on any viewer β€” the card turns green and the channel's subscribers counter ticks up: they're on the list now. Click β–Ά Upload video and notification dots fan out along the wires to only the subscribed viewers β€” their πŸ“₯ inbox badges tick +1 while unsubscribed viewers stay dim, visibly skipped. Now flip a couple of πŸ””s and upload again: a different set gets notified, because the channel never knew who its viewers were β€” it just walks whatever its list says right now. That's the whole pattern: subscribe, get pushed to, unsubscribe, silence.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Build the subscriber list

Hit the πŸ”” Subscribe chip on Ana and Chloe. Their cards turn green and the channel's counter reads subscribers: 2 β€” that's two subscribe() calls landing in the channel's list. Note that nothing has been notified yet: subscribing just registers interest; only a state change triggers the push.

try 02

Upload and watch the push

Click β–Ά Upload video. The channel card pulses, then notification dots travel along the wires to only Ana and Chloe β€” their πŸ“₯ badges tick to 1 while Ben and Dan stay dim, visibly skipped. The explain panel reads 'Notified 2 of 4'. That's notify() walking the list: no polling, no asking β€” the channel pushes to whoever is registered.

try 03

Change the set at runtime

Flip Chloe to πŸ”• Unsubscribe and turn πŸ”” on for Ben and Dan, then upload again. This time the dots reach Ana, Ben and Dan β€” a different set than before, with the πŸ“₯ badges keeping the history (Ana 2, Chloe stuck at 1). The channel's code never changed; only its list did. Hit β†Ί Reset to start clean.

In practice

When to use it β€” and what trips people up

Reach for Observer when...

  • One object's changes must reach a varying set of dependents β€” you can't hard-code the receivers because they come and go at runtime (viewers subscribing, panels opening and closing, plugins registering).
  • You're building event-driven UI β€” clicks, keystrokes, model-changed-so-redraw-the-view. Virtually every UI framework is Observer at its core.
  • You want the producer decoupled from its consumers β€” the subject ships one notify(), and brand-new consumer types plug in later without touching the subject's code.

Skip it when...

  • There's exactly one fixed, known dependent β€” just call it directly. A subscription list for a single permanent listener is ceremony with no payoff.
  • Update cascades are getting hard to follow β€” when observers mutate state that notifies other observers, control flow turns into invisible spaghetti. Centralise the choreography in a [[mediator]] or an explicit event bus instead.
  • Strict ordering or transactional delivery matters β€” plain Observer promises neither; if receivers must run in a fixed order or all-or-nothing, you need a more explicit orchestration.

What it gives you

  • Loose coupling β€” the subject depends on one tiny interface, never on concrete observer classes, so either side can change freely.
  • Open for extension β€” add a brand-new kind of observer (logger, chart, emailer) without touching one line of the subject.
  • Runtime dynamism β€” the set of listeners is data, not code: subscribe and unsubscribe freely while the program runs.
  • Push replaces polling β€” dependents are told at the exact moment of change instead of wastefully asking on a timer.

Common mistakes

  • Lapsed-listener leaks β€” a forgotten unsubscribe keeps dead observers referenced (never garbage-collected) and receiving ghost updates.
  • Unspecified order β€” observers are notified in whatever order the list happens to have; code that relies on order breaks silently.
  • Hidden cascades β€” an update() that changes other subjects can trigger chains of notifications nobody planned or can easily trace.
  • Harder debugging β€” the caller of update() is a loop inside the subject, so 'who made this happen?' isn't visible by reading the observer's code.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// The Observer interface β€” the ONLY thing the channel knows about viewers.
interface Subscriber {
  update(video: string): void;            // the channel calls this
}

// The Subject β€” owns state + the list, and pushes on change.
class Channel {
  private subscribers: Subscriber[] = []; // one-to-many list

  subscribe(s: Subscriber): void {
    this.subscribers.push(s);             // πŸ”” join the list
  }

  unsubscribe(s: Subscriber): void {      // πŸ”• leave the list
    this.subscribers = this.subscribers.filter(x => x !== s);
  }

  upload(title: string): void {
    console.log(`πŸ“Ί uploaded "${title}"`);
    // state changed β†’ walk the list and PUSH (nobody polls)
    for (const s of [...this.subscribers]) s.update(title);
  }
}

// A concrete Observer β€” the channel never sees this class name.
class Viewer implements Subscriber {
  constructor(private name: string) {}
  update(video: string): void {           // push style: data arrives
    console.log(`${this.name} πŸ”” new video: ${video}`);
  }
}

const channel = new Channel();
const ana = new Viewer("Ana");
const ben = new Viewer("Ben");

channel.subscribe(ana);
channel.subscribe(ben);
channel.upload("Observer explained");    // β†’ Ana AND Ben notified

channel.unsubscribe(ben);                // Ben leaves the list...
channel.upload("Strategy explained");    // β†’ only Ana notified

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 06

What is the intent of the Observer pattern?

question 02 / 06

What does the subject (the channel) actually know about its observers?

question 03 / 06

What's the difference between push-style and pull-style notification?

question 04 / 06

You destroy a UI panel but forget to unsubscribe it from the model it was observing. What happens?

question 05 / 06

How does classic Observer differ from pub/sub?

question 06 / 06

Before adding Observer, a dashboard asked the stock service for the latest price every second. What did the pattern change?

0/6 answered