<arch.design/>
Principles/Observer Pattern
{ }CodeArchitecturebeginner1994gofeventsubscribereactive

Observer Pattern

Let a subject notify a dynamic list of dependents automatically when its state changes — without the subject knowing who is listening.

5/5
{ }
Operates at: Code level

Inside a codebase — classes, modules, files

How it works

The Observer pattern (GoF, 1994) defines a one-to-many dependency: when a subject's state changes, all registered observers are notified automatically. The subject holds a list of observers and calls a notification method on each. Observers register and unregister at runtime — the subject never imports or references concrete observer classes.

This is the code-level foundation of reactive and event-driven programming. Modern frameworks implement it everywhere: DOM event listeners, React's useState notifications, RxJS Observable, Node.js EventEmitter, and Kafka consumer groups are all variations of this pattern.

Used at the code level, Observer is the right tool when a domain event (order placed, user registered) needs to trigger multiple reactions (send email, update inventory, track analytics) without coupling the triggering code to the reactions.

Project structure

recommended layout
project structure
src/
├── events/
│ └── order-events.ts # Strongly-typed event union
├── observers/
│ ├── observer.ts # Observer interface
│ ├── email-notifier.ts # Sends confirmation email
│ ├── inventory-updater.ts # Decrements stock
│ └── analytics-tracker.ts # Records metrics
└── order-service.ts # Subject — emits events

Implementation

TypeScript · Go · Rust
// events/order-events.ts
export type OrderEvent =
  | { type: "ORDER_PLACED";    orderId: string; customerId: string; total: number }
  | { type: "ORDER_CANCELLED"; orderId: string; reason: string };

// observers/observer.ts
export interface OrderObserver {
  onEvent(event: OrderEvent): Promise<void>;
}

// order-service.ts — Subject
export class OrderService {
  private observers: OrderObserver[] = [];

  subscribe(observer: OrderObserver)   { this.observers.push(observer); }
  unsubscribe(observer: OrderObserver) {
    this.observers = this.observers.filter(o => o !== observer);
  }

  async placeOrder(order: NewOrder): Promise<string> {
    const saved = await this.repo.save(order);
    await this.notify({
      type: "ORDER_PLACED",
      orderId: saved.id,
      customerId: order.customerId,
      total: order.total,
    });
    return saved.id;
  }

  private async notify(event: OrderEvent) {
    await Promise.all(this.observers.map(o => o.onEvent(event)));
  }
}

// observers/email-notifier.ts
export class EmailNotifier implements OrderObserver {
  async onEvent(event: OrderEvent) {
    if (event.type === "ORDER_PLACED") {
      await this.email.send(`Order ${event.orderId} confirmed — $${event.total}`);
    }
  }
}

// bootstrap — wire observers at startup
const svc = new OrderService(repo);
svc.subscribe(new EmailNotifier(emailService));
svc.subscribe(new InventoryUpdater(inventoryRepo));
svc.subscribe(new AnalyticsTracker(analytics));

Why it matters

Adding a new reaction to a domain event (e.g. adding analytics tracking when an order is placed) should not require editing the order service. Observer makes the subject open/closed: new observers can be registered at startup without modifying the subject.

When to use

  • A state change in one object needs to trigger side effects in others
  • The set of reactions is open — new ones may be added without changing the subject
  • Domain events (order placed, user signed up, payment failed) driving multiple side effects
  • GUI event handling, reactive data flows, domain event publishing

When NOT to use

  • When the dependency chain is fixed and simple — a direct call is clearer
  • Memory leak risk: observers that are not explicitly unsubscribed hold strong references
  • Deep observer chains make execution flow hard to trace

Trade-offs

+

New reactions can be added without modifying the subject

Execution order of observers is non-deterministic and hard to reason about

+

Subject is decoupled from concrete observer implementations

Forgetting to unsubscribe causes memory leaks (especially in long-lived objects)

+

Foundation for event-driven architectures at every scale

Cascading updates through observer chains can cause unexpected side effects

In production

React

useState / useEffect are the framework's observer mechanism — components re-render on state change

RxJS / Angular

Observable.subscribe() is textbook Observer — streams of events, multiple independent subscribers

Node.js

EventEmitter.on() / emit() — the Observer pattern baked into Node's core APIs

Industry adoption

5/5Ubiquitous — used at virtually every scale-focused company.

Related principles