Skip to main content

Observer Pattern ๐Ÿ””

Definition: The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

๐ŸŽฏ Intentโ€‹

Create a subscription mechanism to notify multiple objects about any events that happen to the object they're observing.

๐Ÿค” Problemโ€‹

Imagine you're building an e-commerce application. Customers are interested in a particular product that's currently out of stock. You could have customers check the availability every day, but this would be wasteful. Alternatively, you could send emails to all customers whenever any product becomes available, but this would spam customers who aren't interested in that specific product.

๐Ÿ’ก Solutionโ€‹

The Observer pattern adds a subscription mechanism to the publisher class so individual objects can subscribe to or unsubscribe from event notifications. This mechanism consists of:

  1. Subject (Publisher): Maintains a list of observers and provides methods to subscribe/unsubscribe
  2. Observer (Subscriber): Defines an interface for objects that should be notified of changes
  3. Concrete Subject: Stores state of interest and notifies observers when state changes
  4. Concrete Observer: Implements the Observer interface to keep state consistent with the subject

๐Ÿ—๏ธ Structureโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Subject โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ - observers: Observer[] โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ + subscribe(observer): void โ”‚
โ”‚ + unsubscribe(observer): voidโ”‚
โ”‚ + notify(data): void โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚ notifies
โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ยซinterfaceยป Observer โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ + update(data): void โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚ implements
โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ConcreteObserver โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ - state โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ + update(data): void โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ’ป Code Exampleโ€‹

Basic Implementationโ€‹

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

// Subject (Publisher)
class Subject {
private observers: Observer[] = [];

subscribe(observer: Observer): void {
this.observers.push(observer);
}

unsubscribe(observer: Observer): void {
this.observers = this.observers.filter((obs) => obs !== observer);
}

protected notify(data: unknown): void {
this.observers.forEach((observer) => observer.update(data));
}
}

// Concrete Subject
class NewsAgency extends Subject {
private news: string = '';

setNews(news: string): void {
this.news = news;
this.notify(news);
}

getNews(): string {
return this.news;
}
}

// Concrete Observers
class NewsChannel implements Observer {
constructor(private name: string) {}

update(news: unknown): void {
console.log(`${this.name} broadcasting: ${news}`);
}
}

class OnlinePortal implements Observer {
constructor(private name: string) {}

update(news: unknown): void {
console.log(`${this.name} published online: ${news}`);
}
}

// Usage
const agency = new NewsAgency();
const cnn = new NewsChannel('CNN');
const bbc = new NewsChannel('BBC');
const portal = new OnlinePortal('News Portal');

agency.subscribe(cnn);
agency.subscribe(bbc);
agency.subscribe(portal);

agency.setNews('Breaking: New JavaScript framework released!');
// Output:
// CNN broadcasting: Breaking: New JavaScript framework released!
// BBC broadcasting: Breaking: New JavaScript framework released!
// News Portal published online: Breaking: New JavaScript framework released!

๐ŸŒŸ Real-World Examplesโ€‹

1. Stock Price Monitorโ€‹

interface StockData {
symbol: string;
price: number;
timestamp: Date;
}

class Stock extends Subject {
constructor(
public symbol: string,
private price: number,
) {
super();
}

setPrice(price: number): void {
this.price = price;
this.notify({ symbol: this.symbol, price, timestamp: new Date() });
}
}

class StockDisplay implements Observer {
constructor(private name: string) {}

update(data: unknown): void {
const { symbol, price } = data as StockData;
console.log(`${this.name}: ${symbol} is now $${price}`);
}
}

class StockAlert implements Observer {
constructor(private threshold: number) {}

update(data: unknown): void {
const stockData = data as StockData;
if (stockData.price > this.threshold) {
console.log(`๐Ÿšจ ALERT: ${stockData.symbol} exceeded $${this.threshold}!`);
}
}
}

const appleStock = new Stock('AAPL', 150);
const dashboard = new StockDisplay('Dashboard');
const priceAlert = new StockAlert(180);

appleStock.subscribe(dashboard);
appleStock.subscribe(priceAlert);
appleStock.setPrice(185);

2. React Hook Patternโ€‹

import { useState, useEffect, useCallback } from 'react';

// Generic observable store
class Store<T> {
private listeners: Set<(data: T) => void> = new Set();

constructor(private state: T) {}

getState(): T {
return this.state;
}

setState(next: T): void {
this.state = next;
this.listeners.forEach((fn) => fn(this.state));
}

subscribe(listener: (data: T) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}

// Custom hook
function useStore<T>(store: Store<T>): T {
const [state, setState] = useState<T>(store.getState());

useEffect(() => {
const unsubscribe = store.subscribe(setState);
return unsubscribe; // cleanup on unmount
}, [store]);

return state;
}

// Usage in React component
const userStore = new Store({ name: 'Alice', online: false });

function UserStatus() {
const user = useStore(userStore);
return <div>{user.name} is {user.online ? '๐ŸŸข online' : '๐Ÿ”ด offline'}</div>;
}

3. Custom EventEmitter (Browser-Style)โ€‹

type EventHandler = (...args: any[]) => void;

class EventEmitter {
private events = new Map<string, Set<EventHandler>>();

on(event: string, handler: EventHandler): void {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(handler);
}

off(event: string, handler: EventHandler): void {
this.events.get(event)?.delete(handler);
}

emit(event: string, ...args: unknown[]): void {
this.events.get(event)?.forEach((handler) => handler(...args));
}

once(event: string, handler: EventHandler): void {
const wrapper = (...args: unknown[]) => {
handler(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}

// Usage
const bus = new EventEmitter();

bus.on('user:login', (userId: string) => {
console.log(`User ${userId} logged in`);
});

bus.once('app:ready', () => {
console.log('One-time init');
});

bus.emit('user:login', '42');
bus.emit('app:ready');
bus.emit('app:ready'); // no output - handler removed after first call

โš ๏ธ Common Pitfallsโ€‹

1. Memory Leaks from Forgotten Unsubscriptionsโ€‹

Observers hold references to the subject. If not unsubscribed, they prevent garbage collection.

// โŒ BAD: component unmounts but observer stays subscribed
class BadComponent {
constructor(store: Subject) {
store.subscribe(this); // never unsubscribes
}
}

// โœ… GOOD: explicit cleanup or WeakRef-based subscription
class GoodComponent {
private unsubscribe: () => void;

constructor(store: Subject) {
this.unsubscribe = store.subscribe(this);
}

destroy(): void {
this.unsubscribe(); // clean up
}
}

2. Observer Modifying Collection During Iterationโ€‹

If an observer calls unsubscribe() inside update(), it mutates the array being iterated:

// โœ… FIX: iterate over a snapshot
protected notify(data: unknown): void {
const snapshot = [...this.observers];
snapshot.forEach((observer) => observer.update(data));
}

3. Circular Update Chainsโ€‹

Observer A reacts to a change by triggering a change that Observer B reacts to, which triggers Observer A again โ€” infinite loop.

// โœ… FIX with a re-entrancy guard
private notifying = false;

protected notify(data: unknown): void {
if (this.notifying) return; // prevent re-entrant calls
this.notifying = true;
try {
this.observers.forEach((obs) => obs.update(data));
} finally {
this.notifying = false;
}
}

โšก Performance Considerationsโ€‹

  • Debounce rapid updates: When state changes burst (e.g., typing), notify on a schedule
  • Selective notification: Only notify observers interested in the specific change
  • Microtask batching: Use queueMicrotask or Promise.resolve() to batch synchronous updates
  • Lazy/Observable pattern: Compute derived values only when an observer requests them
class DebouncedSubject extends Subject {
private timer: ReturnType<typeof setTimeout> | null = null;

protected notify(data: unknown): void {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
super.notify(data);
this.timer = null;
}, 16); // ~60fps
}
}

๐Ÿ†š Observer vs Pub/Subโ€‹

These terms are often confused. They're related but distinct:

AspectObserver PatternPublish/Subscribe
CouplingSubject knows about its observersPublishers don't know subscribers
ChannelDirect method callThrough an event bus/broker
FilteringObservers receive all notificationsSubscribers pick specific topics/events
LocationSame process/address spaceCan be cross-process (message queues)
Typical useUI updates, model-view bindingMicroservices, event-driven architecture

๐Ÿ”„ Built-in JavaScript Observersโ€‹

JavaScript ships with observer-like APIs that follow the same pattern:

APIObservesUse Case
EventTargetDOM events (click, keydown, etc.)UI interactivity
MutationObserverDOM tree mutationsLazy-loading, attribute watching
IntersectionObserverElement visibility in viewportInfinite scroll, lazy images
ResizeObserverElement size changesResponsive layouts
PerformanceObserverPerformance timeline entriesReal metrics monitoring
// IntersectionObserver - lazy-load images
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src!;
observer.unobserve(img);
}
});
});

document.querySelectorAll('img[data-src]').forEach((img) => observer.observe(img));

โœ… Prosโ€‹

  • Open/Closed Principle: Introduce new subscriber classes without changing publisher code
  • Loose Coupling: The publisher doesn't need to know concrete classes of subscribers
  • Dynamic Relationships: Establish relations between objects at runtime
  • Broadcast Communication: One-to-many communication is easy to implement

โŒ Consโ€‹

  • Random Order: Subscribers are notified in unspecified order (use priority queues if ordering matters)
  • Memory Leaks: Observers linger if not properly unsubscribed
  • Performance Overhead: Large observer lists slow down notifications
  • Debugging Complexity: Cascading updates are hard to trace

๐ŸŽฏ When to Useโ€‹

  • Model-View architectures: Changes to one object update multiple UI components
  • Event handling systems: Handle events in multiple places
  • Real-time data updates: Stock tickers, chat messages, live feeds
  • State management: Notify components when shared state changes (Redux, Zustand)
  • Caching: Invalidate cached data across multiple cache layers

๐ŸŽญ Variationsโ€‹

1. Push vs Pull Modelโ€‹

// PUSH: Subject sends full data payload
class PushSubject extends Subject {
protected notify(data: unknown): void {
this.observers.forEach((obs) => obs.update(data));
}
}

// PULL: Observer requests the data it needs
class PullSubject extends Subject {
protected notify(): void {
this.observers.forEach((obs) => obs.update(this));
}
}

class PullObserver implements Observer {
update(subject: unknown): void {
const data = (subject as PullSubject).getData(); // pull only what's needed
this.handleData(data);
}
}

2. WeakMap-based Subscription (Auto Cleanup)โ€‹

class WeakRefSubject {
private observers = new Set<WeakRef<Observer>>();

subscribe(observer: Observer): void {
this.observers.add(new WeakRef(observer));
}

protected notify(data: unknown): void {
this.observers.forEach((ref) => {
const observer = ref.deref();
if (observer) observer.update(data);
else this.observers.delete(ref); // auto-remove garbage-collected observers
});
}
}

3. Async Observerโ€‹

interface AsyncObserver {
update(data: unknown): Promise<void>;
}

class AsyncSubject {
private observers: AsyncObserver[] = [];

async notify(data: unknown): Promise<void> {
await Promise.all(this.observers.map((obs) => obs.update(data)));
}
}
  • Mediator: Both promote loose coupling, but Mediator centralizes communication while Observer distributes it
  • Command: Use Observer to notify listeners when commands execute
  • MVC/MVP/MVVM: Observer is fundamental to these architectural patterns for view-model binding
  • State Management (Redux/Zustand): Modern libraries implement Observer under the hood for reactive UI updates

๐Ÿ“š Further Readingโ€‹