Observer (a.k.a. Publish/Subscribe) decouples *who fires an event* from *who reacts to it*. The publisher emits an event; zero or more subscribers receive it. They don't know about each other; the publisher doesn't know how many there are. This is the pattern behind every event bus, message broker, reactive UI, and webhook system. Add a new subscriber and the publisher doesn't change one line.
Any time you have a one-to-many fan-out where the producer shouldn't know about consumers — UI events, domain events, webhooks, log streams, change-data-capture pipelines, multi-team notification systems. The pattern scales from in-process EventEmitters to Kafka clusters spanning continents.
See it
Simulate it
The pattern in one line
The pattern in code
type Listener<T> = (event: T) => void;
class EventEmitter<T> {
private listeners: Listener<T>[] = [];
subscribe(fn: Listener<T>) { this.listeners.push(fn); return () => this.unsubscribe(fn); }
unsubscribe(fn: Listener<T>) { this.listeners = this.listeners.filter((l) => l !== fn); }
emit(event: T) { for (const l of this.listeners) l(event); }
}
const orderEvents = new EventEmitter<OrderPlaced>();
orderEvents.subscribe((e) => sendConfirmationEmail(e));
orderEvents.subscribe((e) => updateAnalytics(e));
orderEvents.subscribe((e) => alertWarehouse(e));
orderEvents.emit(new OrderPlaced(orderId)); // publisher knows nothing
How fan-out flows
flowchart LR
P[Publisher<br/><i>OrderPlaced</i>]
B((Event bus<br/>/ broker))
P -->|emit| B
B --> S1[Email<br/>subscriber]
B --> S2[Analytics<br/>subscriber]
B --> S3[Warehouse<br/>subscriber]
B --> S4[Slack<br/>subscriber]
B -.add later.-> S5[Loyalty<br/>subscriber]
style P fill:#0e7490,stroke:#06b6d4,color:#fff
style B fill:#9a3412,stroke:#f97316,color:#fff
style S1 fill:#365314,stroke:#84cc16,color:#fff
style S2 fill:#365314,stroke:#84cc16,color:#fff
style S3 fill:#365314,stroke:#84cc16,color:#fff
style S4 fill:#365314,stroke:#84cc16,color:#fff
style S5 fill:#1c2333,stroke:#475569,color:#aab3c4,stroke-dasharray: 5 5
The publisher emits once. The bus fans out. Subscribers can be added or removed without anyone else noticing.
Scaling out: from EventEmitter to Kafka
The same pattern, three orders of magnitude apart:
flowchart TB
subgraph Inproc [In-process Observer]
P1[Publisher object]
L1[Listener A]
L2[Listener B]
L3[Listener C]
P1 -->|sync method call| L1
P1 -->|sync method call| L2
P1 -->|sync method call| L3
end
subgraph Distributed [Distributed Pub/Sub]
P2[Publisher service]
K[(Kafka topic<br/>partition 0..N)]
C1[Consumer<br/>service A]
C2[Consumer<br/>service B]
C3[Consumer<br/>service C]
P2 -->|append| K
K -->|pull + offset| C1
K -->|pull + offset| C2
K -->|pull + offset| C3
end
style P1 fill:#0e7490,stroke:#06b6d4,color:#fff
style L1 fill:#365314,stroke:#84cc16,color:#fff
style L2 fill:#365314,stroke:#84cc16,color:#fff
style L3 fill:#365314,stroke:#84cc16,color:#fff
style P2 fill:#0e7490,stroke:#06b6d4,color:#fff
style K fill:#9a3412,stroke:#f97316,color:#fff
style C1 fill:#365314,stroke:#84cc16,color:#fff
style C2 fill:#365314,stroke:#84cc16,color:#fff
style C3 fill:#365314,stroke:#84cc16,color:#fff
| Property | In-process Observer | Distributed Pub/Sub |
|---|---|---|
| Coupling | Decouples identity | Decouples identity, time, location |
| Delivery | Synchronous, in-thread | Async, durable, possibly cross-region |
| Failure mode | Crash kills all listeners | Broker retains events; subscribers replay |
| Ordering | Method-call order | Within-partition only |
| Examples | DOM events, EventEmitter, RxJS | Kafka, RabbitMQ, NATS, SNS+SQS, EventBridge |
Comments 0
Discuss this page. Markdown supported. Be kind.