The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes state, all its dependents (observers) are notified automatically. It’s the foundation of event-driven programming.

The Core Concept

// Type-safe Event Emitter
type EventMap = Record<string, any>;
type EventHandler<T = any> = (data: T) => void;

class TypedEventEmitter<Events extends EventMap> {
  private handlers = new Map<keyof Events, Set<EventHandler>>();

  on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);
    
    // Return unsubscribe function
    return () => this.handlers.get(event)?.delete(handler);
  }

  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    this.handlers.get(event)?.forEach(handler => handler(data));
  }

  once<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void {
    const unsubscribe = this.on(event, (data) => {
      handler(data);
      unsubscribe();
    });
  }
}

Real-World Example: Shopping Cart

// Define events for the cart
interface CartEvents {
  'item:added': { item: CartItem; total: number };
  'item:removed': { itemId: string; total: number };
  'cart:cleared': { previousTotal: number };
  'cart:checkout': { items: CartItem[]; total: number };
}

class ShoppingCart extends TypedEventEmitter<CartEvents> {
  private items: CartItem[] = [];

  addItem(item: CartItem): void {
    this.items.push(item);
    this.emit('item:added', { item, total: this.total });
  }

  removeItem(itemId: string): void {
    this.items = this.items.filter(i => i.id !== itemId);
    this.emit('item:removed', { itemId, total: this.total });
  }

  checkout(): void {
    this.emit('cart:checkout', { items: [...this.items], total: this.total });
    const prevTotal = this.total;
    this.items = [];
    this.emit('cart:cleared', { previousTotal: prevTotal });
  }

  get total(): number {
    return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  }
}

// Observers — each handles its own concern
const cart = new ShoppingCart();

// Analytics observer
cart.on('item:added', ({ item }) => {
  analytics.track('add_to_cart', { productId: item.id, price: item.price });
});

// UI observer
cart.on('item:added', ({ total }) => {
  updateCartBadge(cart.items.length);
  updateCartTotal(total);
});

// Inventory observer
cart.on('cart:checkout', ({ items }) => {
  items.forEach(item => inventory.reserve(item.id, item.quantity));
});

// Logging observer
cart.on('cart:checkout', ({ total }) => {
  logger.info(`Checkout completed: $${total}`);
});

When to Use Observer

  1. Decoupled notifications: The subject doesn’t know (or care) who’s listening
  2. Multiple reactions to one event: Adding a new reaction doesn’t require changing the subject
  3. Plugin architectures: Third-party code can hook into your system via events
  4. UI updates: React to state changes without tight coupling

When NOT to Use Observer

  1. Simple sequential logic: If there’s only ever one handler, a direct function call is clearer
  2. Order-dependent operations: Observers don’t guarantee execution order
  3. Error handling is critical: If one observer fails, should others still run?
  4. Debugging complexity: Event-driven flows can be hard to trace

Practical Tips

// Always clean up subscriptions to prevent memory leaks
class UserDashboard {
  private unsubscribers: (() => void)[] = [];

  mount(cart: ShoppingCart): void {
    this.unsubscribers.push(
      cart.on('item:added', (data) => this.updateUI(data)),
      cart.on('cart:cleared', () => this.resetUI()),
    );
  }

  unmount(): void {
    this.unsubscribers.forEach(unsub => unsub());
    this.unsubscribers = [];
  }
}

The Observer pattern is everywhere — from DOM events to RxJS to Node.js EventEmitter. Master it and you’ll see it in a new light.

“The Observer pattern lets you define a subscription mechanism to notify multiple objects about events that happen to the object they’re observing.” — Refactoring Guru