Observer Pattern
Let a subject notify a dynamic list of dependents automatically when its state changes — without the subject knowing who is listening.
★★★★★5/5Inside 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 layoutsrc/├── 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
useState / useEffect are the framework's observer mechanism — components re-render on state change
Observable.subscribe() is textbook Observer — streams of events, multiple independent subscribers
EventEmitter.on() / emit() — the Observer pattern baked into Node's core APIs
Industry adoption
Related principles
Strategy Pattern
Define a family of interchangeable algorithms behind a common interface so the calling context can swap them at runtime without changing its own code.
Event-Driven Architecture
LiveServices communicate by producing and consuming events asynchronously through a central event bus or message broker.
Event Sourcing
LiveStore state as an immutable log of events rather than the current snapshot — rebuild state by replaying events.