Observer

Define a subscription mechanism so multiple objects are automatically notified when the object they're watching changes state. The foundation of event systems, reactive UIs, and pub/sub architectures.

The Problem

You’re building a weather app. The WeatherData object collects temperature readings. Three displays — CurrentConditionsDisplay, StatisticsDisplay, and ForecastDisplay — need to update whenever the data changes.

Option A: poll each display — inefficient, introduces lag.
Option B: hardcode currentDisplay.update() and statsDisplay.update() inside WeatherDataWeatherData now couples to every display type. Adding a new display means modifying WeatherData.

Neither scales.


The Solution

The Publisher (subject) maintains a list of Subscribers (observers). When something interesting happens, it notifies all subscribers via a standard update() method. Subscribers can subscribe or unsubscribe at any time. The publisher doesn’t know anything about the concrete subscriber types.

Publisher (Subject) - observers: Observer[] + subscribe(o) / unsubscribe(o) + notifyAll() «interface» Observer + update(data) DisplayA DisplayB ... notifies all
Observer — Publisher notifies all subscribed observers; observers can be added or removed at runtime

Real-World Analogy

A magazine subscription. The publisher prints each issue and mails it to every subscriber on their list. Subscribers can join or cancel at any time. The publisher doesn’t deliver to non-subscribers, and doesn’t need to know what subscribers do with the magazine. New subscribers don’t require a new magazine category — they just sign up.


TypeScript Example

// Observer/Subscriber interface
interface WeatherObserver {
  update(temperature: number, humidity: number, pressure: number): void;
}

// Subject/Publisher
class WeatherStation {
  private observers: WeatherObserver[] = [];
  private temperature = 0;
  private humidity    = 0;
  private pressure    = 0;

  subscribe(observer: WeatherObserver): void {
    if (!this.observers.includes(observer)) {
      this.observers.push(observer);
    }
  }

  unsubscribe(observer: WeatherObserver): void {
    this.observers = this.observers.filter(o => o !== observer);
  }

  private notify(): void {
    for (const observer of this.observers) {
      observer.update(this.temperature, this.humidity, this.pressure);
    }
  }

  // When measurements change, notify all observers
  setMeasurements(temp: number, humidity: number, pressure: number): void {
    this.temperature = temp;
    this.humidity    = humidity;
    this.pressure    = pressure;
    console.log(`\n[WeatherStation] New readings: ${temp}°C, ${humidity}% RH, ${pressure} hPa`);
    this.notify();
  }
}

// Concrete observers
class CurrentConditionsDisplay implements WeatherObserver {
  update(temperature: number, humidity: number): void {
    console.log(`  [CurrentConditions] ${temperature}°C, ${humidity}% humidity`);
  }
}

class StatisticsDisplay implements WeatherObserver {
  private readings: number[] = [];

  update(temperature: number): void {
    this.readings.push(temperature);
    const avg = this.readings.reduce((s, t) => s + t, 0) / this.readings.length;
    const max = Math.max(...this.readings);
    const min = Math.min(...this.readings);
    console.log(`  [Statistics] avg=${avg.toFixed(1)}°C, min=${min}°C, max=${max}°C`);
  }
}

class AlertSystem implements WeatherObserver {
  update(temperature: number, humidity: number, pressure: number): void {
    if (temperature > 35) console.log(`  [ALERT] Heat warning: ${temperature}°C`);
    if (pressure < 1000)  console.log(`  [ALERT] Low pressure storm risk: ${pressure} hPa`);
  }
}

// Usage
const station      = new WeatherStation();
const current      = new CurrentConditionsDisplay();
const stats        = new StatisticsDisplay();
const alertSystem  = new AlertSystem();

station.subscribe(current);
station.subscribe(stats);
station.subscribe(alertSystem);

station.setMeasurements(22, 65, 1013);
station.setMeasurements(28, 70, 1010);
station.setMeasurements(37, 80, 998); // triggers heat + low pressure alerts

// Dynamic subscription — unsubscribe mid-way
console.log('\n[Unsubscribing current conditions display]');
station.unsubscribe(current);
station.setMeasurements(25, 60, 1015);
// → current display no longer receives updates

Built-in Observer in Node.js

import { EventEmitter } from 'events';

class OrderService extends EventEmitter {
  placeOrder(orderId: string, amount: number): void {
    console.log(`[OrderService] Order ${orderId} placed`);
    this.emit('order:placed', { orderId, amount });
  }
}

const orders = new OrderService();
orders.on('order:placed', ({ orderId }) => console.log(`[Email] Confirmation sent for ${orderId}`));
orders.on('order:placed', ({ orderId, amount }) => console.log(`[Analytics] $${amount} revenue from ${orderId}`));

orders.placeOrder('ORD-001', 99.99);

When to Use

  • State change in one object requires changing others, and you don’t know how many ahead of time
  • An object should be able to notify others without knowing who they are
  • You’re building event systems, reactive state, or pub/sub messaging
  • Examples: UI event listeners, DOM events, Redux store, RxJS observables, message queues, webhooks

Pros

  • Open/Closed — add new subscribers without changing the publisher
  • Establish relationships between objects at runtime
  • Publishers are loosely coupled from subscriber implementations

Cons

  • Subscribers are notified in an unpredictable order (order depends on when they subscribed)
  • If not managed carefully, publishers can hold references to removed subscribers (memory leaks)
  • Complex chains of notifications can cause hard-to-trace cascades

  • Mediator centralises communication between components; Observer broadcasts to all subscribers
  • Singleton — the publisher or event bus is often a Singleton
  • Command — observer callbacks can be wrapped as Command objects for undoable event handling
  • Iterator can traverse subscriber lists