← Back to principles

Design Principle

Observer Pattern

Learn the Observer pattern: define a one-to-many dependency between objects. When one object changes state, all dependents are notified automatically.

The Observer Pattern defines a one-to-many dependency between objects. When one object changes state, all its dependents are notified and updated automatically.


What is the Observer Pattern?

Observer Pattern provides:

  • Loose coupling: Subject doesn't know concrete observers
  • Dynamic relationships: Add/remove observers at runtime
  • Event notification: Notify multiple observers of changes
  • Broadcast communication: One subject, many observers

Use cases:

  • Model-View architecture (MVC)
  • Event handling systems
  • Publish-subscribe systems
  • Real-time updates
  • UI frameworks (React, Vue)

Structure

Subject (interface)
  ├─ attach(observer)
  ├─ detach(observer)
  └─ notify()

ConcreteSubject
  └─ state
  └─ notify() (notifies all observers)

Observer (interface)
  └─ update()

ConcreteObserver
  └─ update() (reacts to state change)

Examples

Basic Observer Pattern

// Observer interface
interface Observer {
  update(data: any): void;
}

// Subject interface
interface Subject {
  attach(observer: Observer): void;
  detach(observer: Observer): void;
  notify(): void;
}

// Concrete subject
class NewsAgency implements Subject {
  private observers: Observer[] = [];
  private news: string = "";
  
  attach(observer: Observer): void {
    this.observers.push(observer);
  }
  
  detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  
  notify(): void {
    this.observers.forEach(observer => observer.update(this.news));
  }
  
  setNews(news: string): void {
    this.news = news;
    this.notify(); // Notify all observers
  }
}

// Concrete observers
class NewsChannel implements Observer {
  private name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  update(news: string): void {
    console.log(`${this.name} received news: ${news}`);
  }
}

// Usage
const agency = new NewsAgency();
const channel1 = new NewsChannel("CNN");
const channel2 = new NewsChannel("BBC");

agency.attach(channel1);
agency.attach(channel2);

agency.setNews("Breaking: Important news!");
// Output:
// CNN received news: Breaking: Important news!
// BBC received news: Breaking: Important news!

Stock Market Observer

// Observer interface
interface StockObserver {
  update(stock: Stock): void;
}

// Subject
class StockMarket implements Subject {
  private observers: StockObserver[] = [];
  private stocks: Map<string, Stock> = new Map();
  
  attach(observer: StockObserver): void {
    this.observers.push(observer);
  }
  
  detach(observer: StockObserver): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  
  notify(): void {
    this.observers.forEach(observer => {
      this.stocks.forEach(stock => observer.update(stock));
    });
  }
  
  updateStock(symbol: string, price: number): void {
    const stock = this.stocks.get(symbol) || { symbol, price };
    stock.price = price;
    this.stocks.set(symbol, stock);
    this.notify();
  }
}

// Concrete observers
class StockDisplay implements StockObserver {
  update(stock: Stock): void {
    console.log(`Stock ${stock.symbol}: $${stock.price}`);
  }
}

class PriceAlert implements StockObserver {
  private threshold: number;
  
  constructor(threshold: number) {
    this.threshold = threshold;
  }
  
  update(stock: Stock): void {
    if (stock.price > this.threshold) {
      console.log(`ALERT: ${stock.symbol} exceeded $${this.threshold}`);
    }
  }
}

// Usage
const market = new StockMarket();
const display = new StockDisplay();
const alert = new PriceAlert(100);

market.attach(display);
market.attach(alert);

market.updateStock("AAPL", 150);
// Output:
// Stock AAPL: $150
// ALERT: AAPL exceeded $100

Event System

// Event type
type EventType = string;

// Event data
interface EventData {
  [key: string]: any;
}

// Observer (event listener)
interface EventListener {
  handle(eventType: EventType, data: EventData): void;
}

// Event emitter (subject)
class EventEmitter {
  private listeners: Map<EventType, EventListener[]> = new Map();
  
  on(eventType: EventType, listener: EventListener): void {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, []);
    }
    this.listeners.get(eventType)!.push(listener);
  }
  
  off(eventType: EventType, listener: EventListener): void {
    const listeners = this.listeners.get(eventType);
    if (listeners) {
      const index = listeners.indexOf(listener);
      if (index > -1) {
        listeners.splice(index, 1);
      }
    }
  }
  
  emit(eventType: EventType, data: EventData): void {
    const listeners = this.listeners.get(eventType);
    if (listeners) {
      listeners.forEach(listener => listener.handle(eventType, data));
    }
  }
}

// Usage
const emitter = new EventEmitter();

const listener1: EventListener = {
  handle(eventType: EventType, data: EventData): void {
    console.log(`Listener 1: ${eventType}`, data);
  }
};

const listener2: EventListener = {
  handle(eventType: EventType, data: EventData): void {
    console.log(`Listener 2: ${eventType}`, data);
  }
};

emitter.on("user:login", listener1);
emitter.on("user:login", listener2);

emitter.emit("user:login", { userId: 123, username: "john" });
// Output:
// Listener 1: user:login { userId: 123, username: 'john' }
// Listener 2: user:login { userId: 123, username: 'john' }

Common Pitfalls

  • Memory leaks: Observers not detached. Fix: Always detach observers
  • Update order: Order of updates matters. Fix: Document order, use priority
  • Too many observers: Performance issues. Fix: Limit observers, batch updates
  • Circular updates: Observer updates subject. Fix: Prevent circular updates

Interview Questions

Beginner

Q: What is the Observer pattern and when would you use it?

A:

Observer Pattern defines one-to-many dependency between objects. When subject changes, all observers are notified.

Key characteristics:

  • One-to-many: One subject, many observers
  • Loose coupling: Subject doesn't know concrete observers
  • Dynamic: Add/remove observers at runtime
  • Automatic notification: Observers notified automatically

Example:

class Subject {
  private observers: Observer[] = [];
  
  attach(observer: Observer): void {
    this.observers.push(observer);
  }
  
  notify(): void {
    this.observers.forEach(obs => obs.update());
  }
}

Use cases:

  • MVC architecture: Model notifies views
  • Event systems: Event emitters/listeners
  • UI frameworks: React, Vue reactivity
  • Real-time updates: Stock prices, notifications

Benefits:

  • Loose coupling: Subject and observers decoupled
  • Flexibility: Add/remove observers dynamically
  • Broadcast: Notify multiple observers

Intermediate

Q: Explain how the Observer pattern works. How do you handle observer lifecycle and prevent memory leaks?

A:

Observer Pattern Structure:

1. Subject:

class Subject {
  private observers: Observer[] = [];
  
  attach(observer: Observer): void {
    this.observers.push(observer);
  }
  
  detach(observer: Observer): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  
  notify(): void {
    this.observers.forEach(observer => observer.update());
  }
}

2. Observer:

interface Observer {
  update(data: any): void;
}

Preventing Memory Leaks:

1. Always Detach:

class Component {
  private observer: Observer;
  
  constructor(subject: Subject) {
    this.observer = { update: this.handleUpdate.bind(this) };
    subject.attach(this.observer);
  }
  
  destroy(): void {
    // CRITICAL: Detach before destroying
    subject.detach(this.observer);
  }
}

2. Weak References:

class Subject {
  private observers: WeakSet<Observer> = new WeakSet();
  // WeakSet allows garbage collection
}

3. Automatic Cleanup:

class AutoDetachingObserver implements Observer {
  constructor(
    private subject: Subject,
    private callback: Function
  ) {
    subject.attach(this);
  }
  
  update(data: any): void {
    this.callback(data);
  }
  
  destroy(): void {
    this.subject.detach(this);
  }
}

Senior

Q: Design an observer system for a real-time collaborative editor where multiple users can edit a document simultaneously. Handle conflict resolution, network synchronization, and ensure all clients stay in sync.

A:

// Document change event
interface DocumentChange {
  type: "insert" | "delete" | "format";
  position: number;
  content?: string;
  timestamp: number;
  userId: string;
}

// Observer interface
interface DocumentObserver {
  onDocumentChange(change: DocumentChange): void;
  onConflict(change: DocumentChange, localChange: DocumentChange): void;
}

// Subject (Document)
class CollaborativeDocument {
  private observers: DocumentObserver[] = [];
  private content: string = "";
  private changes: DocumentChange[] = [];
  private version: number = 0;
  
  attach(observer: DocumentObserver): void {
    this.observers.push(observer);
  }
  
  detach(observer: DocumentObserver): void {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }
  
  private notify(change: DocumentChange): void {
    this.observers.forEach(observer => observer.onDocumentChange(change));
  }
  
  applyChange(change: DocumentChange): void {
    // Check for conflicts
    const conflict = this.detectConflict(change);
    if (conflict) {
      this.notifyConflict(change, conflict);
      return;
    }
    
    // Apply change
    this.applyChangeToContent(change);
    this.changes.push(change);
    this.version++;
    
    // Notify observers
    this.notify(change);
  }
  
  private detectConflict(change: DocumentChange): DocumentChange | null {
    // Check if change conflicts with recent changes
    for (const existingChange of this.changes.slice(-10)) {
      if (this.isConflict(change, existingChange)) {
        return existingChange;
      }
    }
    return null;
  }
  
  private isConflict(change1: DocumentChange, change2: DocumentChange): boolean {
    // Conflicts if changes overlap in position
    return Math.abs(change1.position - change2.position) < 5 &&
           Math.abs(change1.timestamp - change2.timestamp) < 1000;
  }
  
  private applyChangeToContent(change: DocumentChange): void {
    switch (change.type) {
      case "insert":
        this.content = this.content.slice(0, change.position) +
                      (change.content || "") +
                      this.content.slice(change.position);
        break;
      case "delete":
        this.content = this.content.slice(0, change.position) +
                      this.content.slice(change.position + 1);
        break;
    }
  }
  
  private notifyConflict(change: DocumentChange, conflict: DocumentChange): void {
    this.observers.forEach(observer =>
      observer.onConflict(change, conflict)
    );
  }
  
  getContent(): string {
    return this.content;
  }
  
  getVersion(): number {
    return this.version;
  }
}

// Client observer
class ClientObserver implements DocumentObserver {
  private userId: string;
  private pendingChanges: DocumentChange[] = [];
  
  constructor(userId: string) {
    this.userId = userId;
  }
  
  onDocumentChange(change: DocumentChange): void {
    // Ignore own changes
    if (change.userId === this.userId) {
      return;
    }
    
    // Apply remote change
    this.applyRemoteChange(change);
  }
  
  onConflict(change: DocumentChange, localChange: DocumentChange): void {
    // Resolve conflict (last-write-wins or merge)
    this.resolveConflict(change, localChange);
  }
  
  private applyRemoteChange(change: DocumentChange): void {
    // Update UI with remote change
    console.log(`Remote change from ${change.userId}: ${change.type} at ${change.position}`);
  }
  
  private resolveConflict(change: DocumentChange, localChange: DocumentChange): void {
    // Conflict resolution strategy
    // Option 1: Last-write-wins
    if (change.timestamp > localChange.timestamp) {
      // Accept remote change
      this.applyRemoteChange(change);
    } else {
      // Keep local change, request sync
      this.requestSync();
    }
  }
  
  private requestSync(): void {
    // Request full document sync
  }
}

// Network sync
class NetworkSync {
  private document: CollaborativeDocument;
  
  async syncChange(change: DocumentChange): Promise<void> {
    // Send to server
    await fetch("/api/document/change", {
      method: "POST",
      body: JSON.stringify(change)
    });
  }
  
  async receiveChanges(): Promise<void> {
    // Poll or use WebSocket for changes
    const changes = await fetch("/api/document/changes").then(r => r.json());
    changes.forEach((change: DocumentChange) => {
      this.document.applyChange(change);
    });
  }
}

Features:

  1. Observer pattern: Notify all clients of changes
  2. Conflict detection: Detect conflicting changes
  3. Conflict resolution: Resolve conflicts (last-write-wins, merge)
  4. Network sync: Sync changes across network
  5. Version control: Track document version

Key Takeaways

  • Observer pattern: One-to-many dependency, automatic notification
  • Loose coupling: Subject and observers decoupled
  • Dynamic: Add/remove observers at runtime
  • Use cases: MVC, event systems, real-time updates
  • Memory leaks: Always detach observers
  • Best practices: Use weak references, handle lifecycle, prevent circular updates

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.