Loading content...
The Observer Pattern is fundamentally a two-party relationship: the Subject (the thing being watched) and the Observers (the things doing the watching). Understanding each party's responsibilities, interfaces, and constraints is essential for implementing the pattern correctly.
In the previous pages, we explored the problem and the publish-subscribe solution. Now we'll formalize the participants, examine their structure in detail, and explore advanced implementation considerations that separate production-ready code from textbook examples.
By the end of this page, you will have a complete understanding of Subject and Observer as formal participants: their responsibilities, their contractual obligations, the UML structure of the pattern, and how to implement both abstract and concrete versions. You'll also understand the lifecycle of observer relationships.
The Subject (also called Observable, Publisher, or Event Source) is the object that:
The Subject's responsibilities can be divided into state management (its primary business purpose) and observer management (its pattern-related duty):
| Category | Responsibility | Typical Method | Frequency |
|---|---|---|---|
| Observer Mgmt | Accept new observer registrations | attach() / subscribe() / addObserver() | Once per observer |
| Observer Mgmt | Handle observer unregistration | detach() / unsubscribe() / removeObserver() | Once per observer |
| Observer Mgmt | Maintain observer collection | Internal Set/List field | Ongoing |
| Observer Mgmt | Notify all observers of changes | notify() / publish() / emit() | On every state change |
| State Mgmt | Manage its core state | setState() / setPrice() / etc. | Business-dependent |
| State Mgmt | Provide state access for observers | getState() / getPrice() / etc. | Per observer query |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
/** * Subject Interface (Abstract Observable) * * This interface defines the observer management contract that * all Subjects must implement. It is typically a generic interface * where T represents the type of update data sent to observers. */interface Subject<T = void> { /** * Registers an observer for future notifications. * * Preconditions: * - observer is not null/undefined * - observer implements Observer<T> interface * * Postconditions: * - observer will receive all future update(data) calls * - duplicate registration is either prevented or idempotent * * @param observer The observer to register * @returns void, boolean, or unsubscribe function (implementation choice) */ attach(observer: Observer<T>): void | boolean | (() => void); /** * Removes a previously registered observer. * * Preconditions: * - observer was previously registered with attach() * * Postconditions: * - observer will NOT receive future update(data) calls * - removing a non-registered observer is a no-op * * @param observer The observer to remove */ detach(observer: Observer<T>): void; /** * Notifies all registered observers of a state change. * * Preconditions: * - Subject's state has changed (or notification is otherwise warranted) * * Postconditions: * - Every attached observer has had update(data) called * - Order of notification follows implementation policy * - Exceptions in observers are handled per implementation policy */ notify(): void;} /** * Common extension: Query observer status */interface ExtendedSubject<T> extends Subject<T> { /** * Returns the number of currently attached observers. * Useful for monitoring and debugging. */ getObserverCount(): number; /** * Checks if a specific observer is currently attached. */ hasObserver(observer: Observer<T>): boolean; /** * Removes all observers. Use with caution. */ clearObservers(): void;}In many implementations, the observer management logic is extracted into an abstract base class (like Java's Observable) that concrete subjects extend. This avoids repeating the attach/detach/notify logic in every subject. However, composition can achieve the same result without inheritance.
Let's build a production-quality Subject step by step, addressing each design consideration:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
/** * AbstractSubject - Reusable base class for observable objects * * This class encapsulates all observer management logic so that * concrete subjects only need to focus on their business logic. */abstract class AbstractSubject<T> implements ExtendedSubject<T> { // Using Set for O(1) add/remove and automatic deduplication private readonly observers: Set<Observer<T>> = new Set(); // Optional: Track if we're currently notifying (for reentrancy detection) private isNotifying: boolean = false; // Pending operations during notification (if we want to defer changes) private pendingAttach: Set<Observer<T>> = new Set(); private pendingDetach: Set<Observer<T>> = new Set(); // ========================================== // Observer Management // ========================================== attach(observer: Observer<T>): () => void { // Validate input if (!observer) { throw new Error('Cannot attach null/undefined observer'); } if (typeof observer.update !== 'function') { throw new Error('Observer must implement update() method'); } // Handle registration during notification if (this.isNotifying) { // Defer the attachment until notification completes this.pendingAttach.add(observer); this.pendingDetach.delete(observer); } else { this.observers.add(observer); } // Return unsubscribe function for convenience return () => this.detach(observer); } detach(observer: Observer<T>): void { if (this.isNotifying) { // Defer the detachment until notification completes this.pendingDetach.add(observer); this.pendingAttach.delete(observer); } else { this.observers.delete(observer); } } notify(): void { // Create update data (subclasses define this) const data = this.getUpdateData(); // Guard against reentrant notification const wasAlreadyNotifying = this.isNotifying; this.isNotifying = true; try { // Take a snapshot of observers to iterate // This protects against modification during iteration const observerSnapshot = Array.from(this.observers); for (const observer of observerSnapshot) { // Skip observers that were detached during this notification if (this.pendingDetach.has(observer)) { continue; } try { observer.update(data); } catch (error) { // Isolate observer failures this.handleObserverError(error as Error, observer); } } } finally { this.isNotifying = wasAlreadyNotifying; // Process pending operations if we're the outermost notify if (!wasAlreadyNotifying) { this.processPendingOperations(); } } } private processPendingOperations(): void { // Apply pending detachments for (const observer of this.pendingDetach) { this.observers.delete(observer); } this.pendingDetach.clear(); // Apply pending attachments for (const observer of this.pendingAttach) { this.observers.add(observer); } this.pendingAttach.clear(); } // ========================================== // Extended Interface // ========================================== getObserverCount(): number { return this.observers.size; } hasObserver(observer: Observer<T>): boolean { return this.observers.has(observer); } clearObservers(): void { if (this.isNotifying) { // Mark all for removal for (const observer of this.observers) { this.pendingDetach.add(observer); } } else { this.observers.clear(); } } // ========================================== // Template Methods (for subclasses) // ========================================== /** * Subclasses must implement this to provide update data. */ protected abstract getUpdateData(): T; /** * Subclasses can override to customize error handling. */ protected handleObserverError(error: Error, observer: Observer<T>): void { console.error(`Observer ${observer.constructor.name} threw:`, error); }}Key implementation decisions explained:
isNotifying flag and snapshot iteration prevent modification of the observer collection during notification from causing skipped or duplicate notifications.1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
/** * StockPrice - A concrete Subject for stock price updates */interface StockUpdate { symbol: string; currentPrice: number; previousPrice: number; change: number; changePercent: number; timestamp: Date; volume: number;} class StockPrice extends AbstractSubject<StockUpdate> { private symbol: string; private price: number = 0; private previousPrice: number = 0; private volume: number = 0; private lastUpdated: Date = new Date(); constructor(symbol: string, initialPrice: number = 0) { super(); this.symbol = symbol; this.price = initialPrice; } // Required by AbstractSubject protected getUpdateData(): StockUpdate { const change = this.price - this.previousPrice; const changePercent = this.previousPrice > 0 ? (change / this.previousPrice) * 100 : 0; return { symbol: this.symbol, currentPrice: this.price, previousPrice: this.previousPrice, change, changePercent, timestamp: this.lastUpdated, volume: this.volume, }; } // Business methods setPrice(newPrice: number, volume: number = 0): void { if (newPrice < 0) { throw new Error('Price cannot be negative'); } // Track previous for delta calculations this.previousPrice = this.price; this.price = newPrice; this.volume = volume; this.lastUpdated = new Date(); // Notify observers of the change this.notify(); } // Getters for observers who prefer pull model getSymbol(): string { return this.symbol; } getPrice(): number { return this.price; } getVolume(): number { return this.volume; }}The Observer (also called Subscriber, Listener, or Handler) is the object that:
Unlike the Subject, the Observer interface is remarkably simple—often just a single method:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
/** * Minimal Observer Interface * * The classic, simplest form of Observer. */interface Observer<T = void> { update(data: T): void;} /** * Extended Observer Interface * * Additional lifecycle methods for more control. */interface ExtendedObserver<T> extends Observer<T> { /** * Called when this observer is successfully attached to a subject. * Useful for initialization. */ onAttach?(subject: Subject<T>): void; /** * Called when this observer is detached from a subject. * Useful for cleanup. */ onDetach?(subject: Subject<T>): void; /** * Indicates whether this observer is still interested in updates. * Subject can check this before notifying. */ isActive?(): boolean;} /** * Functional Observer * * In functional programming, observers are often just functions. * This is equivalent to the interface but more concise. */type FunctionalObserver<T> = (data: T) => void; /** * Multiple Callback Observer (RxJS style) * * Separate callbacks for different scenarios. */interface RxObserver<T> { next?(value: T): void; // Called on each value error?(err: Error): void; // Called on error complete?(): void; // Called when stream ends}Observer responsibilities:
| Responsibility | Why It Matters | Common Mistake |
|---|---|---|
| Implement update() | Subject calls this; without it, pattern doesn't work | Forgetting to implement or implementing with wrong signature |
| React quickly | Slow reactions block other observers (in sync model) | Performing expensive operations in update() |
| Handle all data gracefully | Subject may send unexpected data | Assuming data has certain properties without checking |
| Manage own lifecycle | Observer should know when to attach/detach | Memory leaks from never detaching |
| Not modify shared data | Update data may be shared among observers | Mutating the data object, affecting other observers |
When multiple observers receive the same update object, one observer mutating that object affects all subsequent observers. Either design subjects to send immutable data, or make a defensive copy in each observer.
Observers come in many forms, each suited to different use cases. Let's examine several implementation patterns:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
/** * Class-based observers encapsulate both the update logic * and any state the observer needs to maintain. */class TradingAlertObserver implements ExtendedObserver<StockUpdate> { private thresholds: Map<string, { low: number; high: number }>; private notificationService: NotificationService; private lastAlertTime: Map<string, number> = new Map(); private alertCooldownMs: number = 60000; // Don't spam alerts constructor(notificationService: NotificationService) { this.notificationService = notificationService; this.thresholds = new Map(); } setThreshold(symbol: string, low: number, high: number): void { this.thresholds.set(symbol, { low, high }); } update(data: StockUpdate): void { const threshold = this.thresholds.get(data.symbol); if (!threshold) return; // Not tracking this symbol // Check cooldown to avoid spamming const lastAlert = this.lastAlertTime.get(data.symbol) || 0; if (Date.now() - lastAlert < this.alertCooldownMs) { return; } let alertMessage: string | null = null; if (data.currentPrice < threshold.low) { alertMessage = `⚠️ ${data.symbol} has dropped below $${threshold.low} (now $${data.currentPrice.toFixed(2)})`; } else if (data.currentPrice > threshold.high) { alertMessage = `🚀 ${data.symbol} has risen above $${threshold.high} (now $${data.currentPrice.toFixed(2)})`; } if (alertMessage) { this.notificationService.send(alertMessage); this.lastAlertTime.set(data.symbol, Date.now()); } } // Lifecycle hooks onAttach(subject: Subject<StockUpdate>): void { console.log('TradingAlertObserver attached to subject'); } onDetach(subject: Subject<StockUpdate>): void { console.log('TradingAlertObserver detached from subject'); this.lastAlertTime.clear(); } isActive(): boolean { return this.thresholds.size > 0; }}123456789101112131415161718192021222324252627282930313233343536
/** * Functional observers are simple callbacks. * State is captured via closure. */ // Simple logging observerconst createLoggingObserver = (): Observer<StockUpdate> => ({ update: (data) => { console.log(`[${data.timestamp.toISOString()}] ${data.symbol}: $${data.currentPrice.toFixed(2)} (${data.changePercent >= 0 ? '+' : ''}${data.changePercent.toFixed(2)}%)`); }}); // Observer with captured stateconst createMovingAverageObserver = (windowSize: number): Observer<StockUpdate> => { const priceHistory: number[] = []; return { update: (data) => { priceHistory.push(data.currentPrice); // Keep only last N prices if (priceHistory.length > windowSize) { priceHistory.shift(); } const average = priceHistory.reduce((a, b) => a + b, 0) / priceHistory.length; console.log(`${data.symbol} ${windowSize}-period MA: $${average.toFixed(2)}`); } };}; // Usageconst appleStock = new StockPrice('AAPL', 150);appleStock.attach(createLoggingObserver());appleStock.attach(createMovingAverageObserver(5));appleStock.attach(createMovingAverageObserver(20));123456789101112131415161718192021222324252627
/** * For simple cases, observers can be defined inline. * This is common in UI frameworks and event handling. */ // If Subject accepts functional observersappleStock.attach({ update: (data) => { document.getElementById('price-display')!.textContent = `$${data.currentPrice.toFixed(2)}`; }}); // Even simpler if Subject accepts plain functionsappleStock.subscribe((data) => { document.getElementById('price-display')!.textContent = `$${data.currentPrice.toFixed(2)}`;}); // Common React pattern (where setState is the observer)useEffect(() => { const unsubscribe = stockService.subscribe((update) => { setPrice(update.currentPrice); }); return () => unsubscribe(); // Cleanup on unmount}, []);Use class-based when observers need significant internal state, configuration, or lifecycle management. Use functional when the logic is stateless or state can be captured in a closure. Use inline for simple, one-off reactions.
The Observer Pattern has a formal structure documented in the Gang of Four book. Understanding this structure helps you recognize the pattern in existing code and communicate about it with other engineers:
123456789101112131415161718192021222324252627282930313233343536373839404142
┌──────────────────────────────────────────────────────────────────┐│ <<interface>> ││ Subject │├──────────────────────────────────────────────────────────────────┤│ + attach(observer: Observer): void ││ + detach(observer: Observer): void ││ + notify(): void │└──────────────────────────┬───────────────────────────────────────┘ │ implements ▼┌──────────────────────────────────────────────────────────────────┐│ ConcreteSubject │├──────────────────────────────────────────────────────────────────┤│ - observers: Set<Observer> ││ - subjectState: StateType │├──────────────────────────────────────────────────────────────────┤│ + attach(observer: Observer): void ││ + detach(observer: Observer): void ││ + notify(): void ││ + getState(): StateType ││ + setState(state: StateType): void │└──────────────────────────────────────────────────────────────────┘ │ │ notifies ─────────────────┐ │ │ ▼ │┌──────────────────────────────────────────────────────────────────┐│ <<interface>> ││ Observer │├──────────────────────────────────────────────────────────────────┤│ + update(subject?: Subject): void │└──────────────────────────┬───────────────────────────────────────┘ │ implements ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼┌──────────────┐ ┌──────────────┐ ┌──────────────┐│ConcreteObs A │ │ConcreteObs B │ │ConcreteObs C │├──────────────┤ ├──────────────┤ ├──────────────┤│- observerState│ │- observerState│ │- observerState│├──────────────┤ ├──────────────┤ ├──────────────┤│+ update() │ │+ update() │ │+ update() │└──────────────┘ └──────────────┘ └──────────────┘| Participant | Role | Key Elements |
|---|---|---|
| Subject (interface) | Defines observer management contract | attach(), detach(), notify() methods |
| ConcreteSubject | Implements Subject, holds state of interest | Observer collection, state, state accessors |
| Observer (interface) | Defines update callback contract | update() method signature |
| ConcreteObserver | Implements Observer, reacts to updates | update() implementation, optional state |
The Subject aggregates Observers (it maintains a collection of them). ConcreteSubject implements Subject. ConcreteObservers implement Observer. There's no inheritance hierarchy between Subject and Observer—they're independent contracts connected only by the reference in the Subject's collection.
One of the most critical—and often overlooked—aspects of the Observer Pattern is lifecycle management. Observers must be properly attached when they need updates and properly detached when they're done. Failure to manage this correctly leads to memory leaks and ghost notifications.
1234567891011121314151617181920212223242526272829303132333435363738
// ❌ MEMORY LEAK: Observer never detached class UserInterface { private stockPrice: StockPrice; private displayObserver: Observer<StockUpdate>; constructor(stockPrice: StockPrice) { this.stockPrice = stockPrice; // Create observer with reference to 'this' this.displayObserver = { update: (data) => { // This closure captures 'this' (the UserInterface) this.updateDisplay(data); } }; // Attach observer this.stockPrice.attach(this.displayObserver); } private updateDisplay(data: StockUpdate): void { // Update UI } // No destroy/cleanup method exists! // When UserInterface is "discarded", the observer remains attached // The closure holds a reference to UserInterface, preventing garbage collection} // Usage pattern that causes leak:function showTemporaryPanel(stockPrice: StockPrice) { const panel = new UserInterface(stockPrice); // Panel is shown, then user closes it // But panel is never garbage collected because: // stockPrice.observers -> displayObserver -> panel // Each call to showTemporaryPanel leaks another panel!}123456789101112131415161718192021222324252627282930313233343536373839404142
// ✅ CORRECT: Explicit cleanup with unsubscribe function class UserInterface { private unsubscribe: (() => void) | null = null; constructor(stockPrice: StockPrice) { // attach() returns unsubscribe function this.unsubscribe = stockPrice.attach({ update: (data) => this.updateDisplay(data) }); } private updateDisplay(data: StockUpdate): void { // Update UI } destroy(): void { // Clean up observer subscription if (this.unsubscribe) { this.unsubscribe(); this.unsubscribe = null; } // Other cleanup... }} // Usage with cleanup:function showTemporaryPanel(stockPrice: StockPrice): () => void { const panel = new UserInterface(stockPrice); // Return cleanup function return () => { panel.destroy(); // Unsubscribes observer // Now panel can be garbage collected };} // Caller is responsible for cleanup:const cleanup = showTemporaryPanel(appleStock);// Later, when panel is no longer needed:cleanup();| Pattern | When to Use | Example |
|---|---|---|
| Unsubscribe function | Modern functional code, React hooks | const unsub = subject.attach(obs); unsub(); |
| Subscription object | When you need to track subscription state | const sub = subject.subscribe(...); sub.unsubscribe(); |
| Dispose pattern | When observer has multiple cleanup tasks | observer.dispose() handles all cleanup |
| Automatic via WeakRef | When you want automatic cleanup on GC | Subject holds WeakRef to observers |
| Scope-based | When lifetime tied to a scope | Using a try-finally or component lifecycle |
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
/** * Experimental: Using WeakRef for automatic observer cleanup. * Observer is automatically removed when garbage collected. * Note: Finalization timing is not guaranteed! */class WeakSubject<T> { private observers: Set<WeakRef<Observer<T>>> = new Set(); private registry: FinalizationRegistry<WeakRef<Observer<T>>>; constructor() { // When an observer is GC'd, remove its WeakRef from our set this.registry = new FinalizationRegistry((weakRef) => { this.observers.delete(weakRef); }); } attach(observer: Observer<T>): void { const weakRef = new WeakRef(observer); this.observers.add(weakRef); this.registry.register(observer, weakRef); } notify(): void { const data = this.getUpdateData(); for (const weakRef of this.observers) { const observer = weakRef.deref(); if (observer) { observer.update(data); } else { // Observer was garbage collected, remove WeakRef this.observers.delete(weakRef); } } } protected getUpdateData(): T { throw new Error('Subclass must implement'); }} // Note: WeakRef cleanup is non-deterministic and depends on GC timing.// This approach is rarely used in practice because:// 1. GC timing is unpredictable// 2. You might still receive notifications after "disposing"// 3. Explicit unsubscribe is more reliableWhile WeakRef is interesting academically, production code should always use explicit unsubscribe/dispose patterns. Garbage collection timing is not guaranteed, and leaving observer cleanup to GC can cause unpredictable behavior.
Let's formalize the relationship between Subject and Observer with contracts that both sides must honor:
If your implementation provides stronger guarantees (e.g., guaranteed order, guaranteed synchronous notification), document them clearly. Observers will rely on whatever behavior your implementation exhibits, so make the contract explicit.
We've now thoroughly examined both participants in the Observer Pattern—the Subject that publishes changes and the Observers that subscribe to them. Let's consolidate our understanding:
What's next:
We've covered the pull model implicitly (observer queries subject) and the push model (subject sends data). In the next page, we'll examine these two models in depth—their tradeoffs, when to use each, and how to combine them effectively.
You now have a comprehensive understanding of Subject and Observer participants. You can implement production-ready subjects with reentrancy protection and error isolation, choose appropriate observer implementation patterns, and manage observer lifecycles correctly to avoid memory leaks.