Skip to main content

Observer Pattern

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


The Problem​

You're building a stock trading app. When a stock price changes, multiple components need to react: the chart updates, alerts trigger, the portfolio recalculates.

The naive approach:

// stock-service.ts β€” the publisher knows ALL its consumers
import { updateChart } from "./chart";
import { checkAlerts } from "./alerts";
import { recalcPortfolio } from "./portfolio";

function updateStockPrice(stock: Stock, newPrice: number) {
stock.price = newPrice;
updateChart(stock); // tight coupling
checkAlerts(stock); // tight coupling
recalcPortfolio(stock); // tight coupling
// ... every new feature = modify this file and add another import
}

Every new subscriber = modify the publisher. The publisher must know about every consumer.


The Solution​

Publishers don't know their subscribers. They just emit events. Subscribers register independently:

import { createObservable } from "@pithos/core/eidos/observer/observer";

type PriceUpdate = { symbol: string; price: number };

const priceChanged = createObservable<PriceUpdate>();

// Chart subscribes
priceChanged.subscribe(({ symbol, price }) => {
chart.addPoint(symbol, price);
});

// Alert system subscribes (doesn't know about chart)
priceChanged.subscribe(({ symbol, price }) => {
if (price > thresholds[symbol]) sendAlert(`${symbol} spike!`);
});

// Portfolio subscribes (doesn't know about chart or alerts)
priceChanged.subscribe(({ symbol, price }) => {
portfolio.recalculate(symbol, price);
});

// Publisher doesn't know who's listening
// TS enforces the payload shape β€” emit({ symbol: 123 }) is a compile error
priceChanged.notify({ symbol: "AAPL", price: 150 });

New subscriber? Just call .subscribe(). No publisher changes needed. Three independent systems react to the same event without knowing about each other.


Live Demo​

Code
/**
* Stock ticker using Pithos Observer pattern.
*
* The market ALWAYS runs. Subscribers receive updates only when subscribed.
* Unsubscribe = you stop receiving, but the market doesn't stop.
*/

import { createObservable } from "@pithos/core/eidos/observer/observer";
import type { Stock, StockUpdate } from "./types";
import type { Index } from "./data";
import { STOCKS as INITIAL_STOCKS, MARKET_START_HOUR, MINUTES_PER_TICK, INDICES as INITIAL_INDICES } from "./data";

export type { StockUpdate, Alert, Stock } from "./types";
export type { Index } from "./data";
export { STOCKS, ALERT_THRESHOLD, HOLDINGS, INDICES, MARKET_START_HOUR, MINUTES_PER_TICK, HISTORY_SIZE } from "./data";

/** The observable β€” the market emits updates, subscribers listen */
export const stockTicker = createObservable<StockUpdate>();

let tickIndex = 20;

export function getMarketTime(): string {
const totalMinutes = MARKET_START_HOUR * 60 + tickIndex * MINUTES_PER_TICK;
const h = Math.floor(totalMinutes / 60) % 24;
const m = totalMinutes % 60;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}

function randomChange(currentPrice: number): number {
return currentPrice * ((Math.random() - 0.5) * 6 / 100);
}

/** Internal market state β€” always running */
let marketStocks: Stock[] = INITIAL_STOCKS.map((s) => ({ ...s }));
let marketIndices: Index[] = INITIAL_INDICES.map((i) => ({ ...i }));

/** Tick the market: updates prices and notifies subscribers */
export function marketTick(): void {
tickIndex++;

marketStocks = marketStocks.map((stock) => {
const change = randomChange(stock.price);
const newPrice = Math.max(1, stock.price + change);
const changePercent = ((newPrice - stock.previousPrice) / stock.previousPrice) * 100;

// Notify subscribers
queueMicrotask(() => {
stockTicker.notify({
symbol: stock.symbol,
price: newPrice,
change: newPrice - stock.price,
changePercent,
timestamp: Date.now(),
});
});

return { ...stock, price: newPrice };
});

marketIndices = marketIndices.map((idx) => {
const pctChange = (Math.random() - 0.5) * 0.4;
return { ...idx, price: Math.max(1, idx.price + idx.price * (pctChange / 100)) };
});
}

/** Get current market indices (always live) */
export function getMarketIndices(): Index[] {
return marketIndices;
}

/** Reset everything */
export function resetMarket(): void {
tickIndex = 20;
marketStocks = INITIAL_STOCKS.map((s) => ({ ...s }));
marketIndices = INITIAL_INDICES.map((i) => ({ ...i }));
}
Result

Real-World Analogy​

YouTube subscriptions. You subscribe to a channel, you get notified when they upload. The creator doesn't know how many subscribers will watch. You can unsubscribe anytime. The creator and viewers are completely decoupled.


When to Use It​

  • Changes in one object should trigger updates in others
  • You don't know in advance how many objects need to react
  • You want loose coupling between event producers and consumers

When NOT to Use It​

If you have a single consumer that always reacts the same way, a direct function call is clearer. Observer adds indirection that's only worth it when subscribers are dynamic or unknown at compile time.


API​

  • createObservable β€” Create a typed event emitter with subscribe, notify, once, safeNotify, size, and clear