Behavioral Pattern (GoF)

Observer

Defines a one-to-many dependency between objects: when the subject changes state, all its observers are notified and updated automatically — without the subject needing to know each of them.

Intent

Establish a one-to-many dependency relationship so that, when the subject (Subject / Observable) changes its internal state, all registered observers are notified automatically. The subject doesn't know the concrete types of the observers — it communicates only through the Observer interface.

Cataloged by the GoF (1994) as a behavioral pattern, Observer is the conceptual foundation of event systems, data-binding in UI frameworks (React, Vue, Angular), reactive streams (RxJS, ReactiveX), and any notification mechanism where the data producer must not be tightly coupled to its consumers.

Problem

Imagine a weather station that measures temperature, humidity, and pressure. Multiple displays need to show this data: a current-readings panel, a historical chart, and an alerts display. The naive approach would couple the station directly to each display:

// Naive approach — DON'T do this:
class WeatherStation {
  private currentPanel: CurrentPanel;
  private historyChart: HistoryChart;
  private alertsDisplay: AlertsDisplay;

  update(temp: number, humidity: number, pressure: number): void {
    this.currentPanel.show(temp, humidity, pressure);
    this.historyChart.record(temp, humidity, pressure);
    this.alertsDisplay.check(temp, humidity, pressure);
    // Every new display requires modifying this class.
  }
}

Adding or removing a display requires modifying WeatherStation — a violation of the Open/Closed Principle. Displays also can't be registered or removed at runtime. Observer solves both problems: the station doesn't know the displays, it just publishes changes; displays subscribe and unsubscribe independently.

Solution

Observer organizes the code into three participants:

  1. Subject (Observable): keeps a list of observers and offers methods to register them (subscribe) and remove them (unsubscribe). When its state changes, it calls notify to walk the list and inform each observer.
  2. Observer (interface): declares the update method that the Subject calls. E.g.: update(data).
  3. ConcreteObserver: implements Observer and reacts to the notification according to its responsibility — displaying data, logging, sending an alert.

Push vs Pull

There are two approaches to what the Subject sends in the notification:

  • Push: the Subject sends the current data directly in the notification argument — update(temp, humidity, pressure). It's more direct, but it couples the Observer's signature to the exact shape of the Subject's data. When the state grows, the Observer interface changes.
  • Pull: the Subject notifies without data (or with a reference to itself) — update(subject). Each Observer asks the Subject only for what it needs. More flexible: new data can be added to the Subject without breaking the Observer's signature. The cost is that the Observer needs to know the Subject's interface to pull data.

Observer vs Pub/Sub

Observer and Publish/Subscribe are frequently confused, but they differ in coupling:

  • Observer: the Subject directly knows its Observers (it keeps the list). There's direct coupling Subject ↔ Observer, but not between Observers.
  • Pub/Sub: introduces a broker (event bus) between publishers and subscribers. Publishers and subscribers don't know each other — they communicate only through the channel/topic. Decoupling is total, but flow traceability is lower.

Use Observer when the number of observers is manageable and the Subject–Observer relationship is direct. Use Pub/Sub when communication needs to cross modules or services without direct coupling.

Structure

         «interface»
           Observer
    ┌──────────────────────────┐
    │ + update(data: T): void  │
    └──────────────────────────┘
              ▲
   ┌──────────┴─────────────────────┐
   │                                │
CurrentPanel                HistoryChart
(ConcreteObserver)        (ConcreteObserver)


           Subject
  ┌───────────────────────────────────────────┐
  │ - observers: Observer[]                   │
  │ + subscribe(o: Observer): void            │
  │ + unsubscribe(o: Observer): void          │
  │ # notify(): void                          │
  └───────────────────────────────────────────┘
              ▲
  WeatherStation (ConcreteSubject)
  ┌───────────────────────────────────────────┐
  │ - temperature: number                     │
  │ - humidity: number                        │
  │ + setReading(t, h): void                  │
  │   → calls this.notify()                   │
  └───────────────────────────────────────────┘


Notification flow (push model):

  station.setReading(25, 60)
    → notify()
      → panel.update({ temperature: 25, humidity: 60 })
      → chart.update({ temperature: 25, humidity: 60 })
      → alerts.update({ temperature: 25, humidity: 60 })

Code examples

Example 1 — Weather station with multiple displays

A complete implementation with a generic Subject, subscribe/unsubscribe and notification. Uses the push model with a typed data object.

// ── Interfaces ────────────────────────────────────────────────
interface WeatherData {
  temperature: number;
  humidity: number;
}

interface Observer<T> {
  update(data: T): void;
}

// ── Generic Subject ──────────────────────────────────────────
class Subject<T> {
  private readonly observers: Set<Observer<T>> = new Set();

  subscribe(observer: Observer<T>): void {
    this.observers.add(observer);
  }

  unsubscribe(observer: Observer<T>): void {
    this.observers.delete(observer);
  }

  protected notify(data: T): void {
    for (const observer of this.observers) {
      observer.update(data);
    }
  }
}

// ── ConcreteSubject ───────────────────────────────────────────
class WeatherStation extends Subject<WeatherData> {
  private temperature = 0;
  private humidity = 0;

  setReading(temperature: number, humidity: number): void {
    this.temperature = temperature;
    this.humidity = humidity;
    this.notify({ temperature: this.temperature, humidity: this.humidity });
  }
}

// ── ConcreteObservers ─────────────────────────────────────────
class CurrentPanel implements Observer<WeatherData> {
  update(data: WeatherData): void {
    console.log(
      `[Panel] Temp: ${data.temperature}C  Humidity: ${data.humidity}%`
    );
  }
}

class AlertsDisplay implements Observer<WeatherData> {
  update(data: WeatherData): void {
    if (data.temperature > 35) {
      console.log("[ALERT] Critical temperature:", data.temperature + "C");
    }
    if (data.humidity < 30) {
      console.log("[ALERT] Low humidity:", data.humidity + "%");
    }
  }
}

class HistoryChart implements Observer<WeatherData> {
  private readonly history: WeatherData[] = [];

  update(data: WeatherData): void {
    this.history.push(data);
    console.log(`[Chart] Records: ${this.history.length}`);
  }

  getData(): readonly WeatherData[] {
    return this.history;
  }
}

// ── Usage ────────────────────────────────────────────────────
const station = new WeatherStation();
const panel = new CurrentPanel();
const alerts = new AlertsDisplay();
const chart = new HistoryChart();

station.subscribe(panel);
station.subscribe(alerts);
station.subscribe(chart);

station.setReading(25, 60);
// [Panel] Temp: 25C  Humidity: 60%
// [Chart] Records: 1

station.setReading(38, 25);
// [Panel] Temp: 38C  Humidity: 25%
// [ALERT] Critical temperature: 38C
// [ALERT] Low humidity: 25%
// [Chart] Records: 2

// Unsubscribe the panel — it stops receiving notifications.
station.unsubscribe(panel);
station.setReading(20, 70);
// [Chart] Records: 3  (panel was not notified)

Example 2 — Typed event system (pull model)

In the pull model, the Subject only notifies that something changed (passing a reference to itself). Each Observer queries the data it needs. This pattern is more flexible when the Subject has rich state and different Observers need distinct subsets of information.

// Pull model: the Observer receives a reference to the Subject
// and "pulls" only the data it needs.

interface OrderObserver {
  onOrderUpdated(order: Order): void;
}

class Order {
  private readonly observers: Set<OrderObserver> = new Set();

  private _status: string = "draft";
  private _items: string[] = [];

  subscribe(o: OrderObserver): void    { this.observers.add(o); }
  unsubscribe(o: OrderObserver): void  { this.observers.delete(o); }

  get status(): string           { return this._status; }
  get items(): readonly string[] { return this._items; }
  get total(): number            { return this._items.length * 50; } // $50/item (simplified)

  addItem(item: string): void {
    this._items.push(item);
    this.notify();
  }

  advanceStatus(newStatus: string): void {
    this._status = newStatus;
    this.notify();
  }

  private notify(): void {
    for (const o of this.observers) {
      o.onOrderUpdated(this); // passes `this` — the Observer pulls what it needs
    }
  }
}

// Observer that only cares about the status:
class StatusLogger implements OrderObserver {
  onOrderUpdated(order: Order): void {
    console.log(`[Log] Current status: ${order.status}`);
  }
}

// Observer that only cares about the total:
class TotalCalculator implements OrderObserver {
  onOrderUpdated(order: Order): void {
    console.log(`[Total] $${order.total.toFixed(2)} (${order.items.length} items)`);
  }
}

// ── Usage ────────────────────────────────────────────────────
const order = new Order();
order.subscribe(new StatusLogger());
order.subscribe(new TotalCalculator());

order.addItem("Keyboard");
// [Log] Current status: draft
// [Total] $50.00 (1 items)

order.addItem("Mouse");
// [Log] Current status: draft
// [Total] $100.00 (2 items)

order.advanceStatus("confirmed");
// [Log] Current status: confirmed
// [Total] $100.00 (2 items)

When to use

  • When a change in one object requires updating others and you don't know how many or which ones at design time. Notification systems, live data feeds, processing pipelines.
  • When observers need to be registered and removed dynamically at runtime — modules that enter and leave the system without requiring a recompile.
  • To implement MVC/MVP: the Model is the Subject; Views are Observers that update automatically when the Model changes. It's the foundation of data-binding in Angular, Vue and similar frameworks.
  • When you want to decouple the data producer from its consumers without introducing an intermediate broker (use Pub/Sub for that).

When to avoid

  • When notification order matters and is critical: Observer doesn't guarantee notification order by default. If Observer B depends on Observer A having already processed the notification, the pure pattern doesn't guarantee that — extra logic is needed.
  • When observers are few, fixed, and known: a direct call is simpler and more traceable than setting up the Observer infrastructure.
  • When cascading notifications are likely: if an Observer modifies the Subject during update, triggering new notifications, the system can enter loops or exhibit unpredictable behavior.

Pros and cons

Pros

  • Decouples the Subject from its Observers — the Subject doesn't need to know their concrete types.
  • Observers can be added and removed at runtime without changing the Subject.
  • Follows the Open/Closed Principle — new Observers don't require modifying the Subject.
  • Natural foundation for reactive systems, data-binding, and event-driven architecture.

Cons

  • Memory leaks: Observers that aren't removed keep the Subject (and themselves) alive — a classic pitfall in garbage-collected languages.
  • Indeterminate notification order: dependencies between Observers are hard to manage.
  • Cascading updates: an Observer that modifies the Subject can trigger unexpected notifications.
  • Hard to trace the data flow: with many Observers, understanding "who triggered what" requires specific debugging tools.

Common pitfalls

1. Memory leaks from not unsubscribing

The most frequent pitfall: registering an Observer and forgetting to call unsubscribe when it's no longer needed. As long as the Subject exists, it keeps a strong reference to the Observer — preventing the GC from collecting the object. In long-lived environments (servers, SPAs), the accumulation of "dead" Observers causes memory leaks.

Solution: use a Set (makes removal by reference easier), return a cancellation function (disposable) from subscribe, or use WeakRef for optional Observers. In TypeScript with RxJS, use takeUntil and unsubscribe in the component's lifecycle.

2. Cascading updates

If an Observer calls a Subject method that triggers another round of notifications, the result can be an infinite loop or an unpredictable update order. Rule of thumb: Observers shouldn't modify the Subject's state during update. If needed, use an event-queue mechanism (next tick, microtask, setTimeout) to process the change outside the current notification cycle.

3. Excessive notifications

A Subject that notifies on every small change (e.g.: every keystroke in a text field) can overload slow Observers. Mitigation techniques: debounce (groups changes within a time window), dirty flag (notifies only if the value actually changed), or batch notification (accumulates changes and notifies once at the end of the cycle).

4. Confusing Observer with Pub/Sub

Observer implies direct knowledge: the Subject keeps the list of Observers and calls them directly. Pub/Sub uses an intermediate broker (EventEmitter, Message Bus) that fully decouples publishers from subscribers. Use Observer when direct coupling is acceptable; use Pub/Sub when you need decoupling between independent modules or services.

Related patterns

Observer relates to other behavioral and structural patterns:

Strategy and Observer both use interfaces for decoupling, but for different purposes: Strategy swaps algorithms in the context; Observer distributes state notifications to multiple receivers. State also manages implicit notifications when the object changes state — in some implementations, the State notifies an Observer or uses a Subject to propagate the change. Mediator centralizes communication between objects, eliminating direct references between them — it's an alternative to Observer when many objects need to communicate bidirectionally, since Observer still couples the Subject to its Observers through the registration list.