A class hardcodes calls to specific collaborators whenever something happens — `this.inventory.reserve(...)`, `this.analytics.recordAdd(...)`. Adding a new interested party means editing the publisher; the publisher's responsibilities expand with every new consumer; tests must mock every downstream collaborator.
The publisher emits events; listeners subscribe at the composition root and receive notifications without the publisher knowing they exist. Adding a consumer is a new listener + one subscription line, with no publisher edit.
Before the refactoring
// Cart knows which collaborators to notify and how.class Cart {constructor(inventory, analytics, shipping) {this.items = [];this.inventory = inventory;this.analytics = analytics;this.shipping = shipping;}addItem(item, quantity) {this.items.push({ item, quantity });this.inventory.reserve(item, quantity);this.analytics.recordAdd(item, quantity);this.shipping.updateEstimate(this.items);}}
After the refactoring
// Cart publishes events; listeners subscribe at the composition root.class Cart {constructor() {this.items = [];this.listeners = [];}subscribe(listener) {this.listeners.push(listener);}addItem(item, quantity) {this.items.push({ item, quantity });this.notify({ type: 'item-added', item, quantity, cart: this });}notify(event) {this.listeners.forEach((listener) => listener.handle(event));}}class InventoryListener {constructor(inventory) { this.inventory = inventory; }handle(event) {if (event.type === 'item-added') this.inventory.reserve(event.item, event.quantity);}}class AnalyticsListener {constructor(analytics) { this.analytics = analytics; }handle(event) {if (event.type === 'item-added') this.analytics.recordAdd(event.item, event.quantity);}}class ShippingListener {constructor(shipping) { this.shipping = shipping; }handle(event) {if (event.type === 'item-added') this.shipping.updateEstimate(event.cart.items);}}const cart = new Cart();cart.subscribe(new InventoryListener(inventory));cart.subscribe(new AnalyticsListener(analytics));cart.subscribe(new ShippingListener(shipping));
Hard-coded notifications are Insider Trading — the publisher knows too much about who cares about what, and tests can't isolate the publisher from its consumers. Each new consumer couples the publisher to more of the domain it shouldn't have to know.
Observer scatters the system's reactive logic across loosely-coupled listeners; the agent or developer must read the subscription site to know what runs when an event fires. Event ordering becomes implicit; debugging requires recovering subscription order. Memory leaks via uncleaned subscriptions are a real failure mode.
The publisher's responsibilities shrink to 'do the work, announce completion'; consumers attach without modifying the publisher. New consumers don't risk breaking existing ones; tests for the publisher mock no consumers, tests for consumers exercise event handling in isolation.
An Observer system without explicit event types or ordering guarantees turns into a debugging nightmare — the agent must trace 'who listens to what, in what order' across the whole subscription graph. The pattern earns its keep when subscribers are stable, semantic event types are documented, and ordering doesn't matter.