A component fires an event when its state changes. Other components subscribe to that event type and respond independently. No component knows who else is listening — or whether anyone is.
This is the core of event-driven architecture: producers and consumers share a contract (the event schema), not a dependency.
The three actors
Producer — emits events when something happens. Knows nothing about consumers.
Event bus — routes events to registered subscribers by type.
Consumer — registers a handler for one or more event types, performs side effects or state updates.
class EventBus {
constructor() { this.subscribers = {}; }
on(type, handler) {
(this.subscribers[type] ??= []).push(handler);
}
emit(type, payload) {
(this.subscribers[type] ?? []).forEach(fn => fn(payload));
}
}
Usage:
const bus = new EventBus();
bus.on('order.created', ({ orderId }) => reserveInventory(orderId));
bus.on('order.created', ({ orderId }) => sendConfirmationEmail(orderId));
bus.emit('order.created', { orderId: 'ord-991', total: 84.00 });
One emit, two independent consumers. Neither knows the other exists.
Delivery guarantees
| Guarantee | Behavior | Requirement |
|---|---|---|
| At-most-once | Fire and forget. No retries. | Lossy-tolerant flows |
| At-least-once | Retry on failure. Duplicates possible. | Idempotent consumers |
| Exactly-once | Coordinated deduplication. | Distributed consensus — rarely worth the cost |
Most production systems target at-least-once and enforce idempotency at the consumer. An idempotent consumer produces the same outcome whether it receives an event once or ten times. Typically achieved with a deduplication key.
const processed = new Set();
bus.on('order.created', ({ orderId }) => {
if (processed.has(orderId)) return;
processed.add(orderId);
reserveInventory(orderId);
});
Event schema discipline
An event should contain enough context to act on, without embedding business logic. Keep it factual: what happened, to what, when.
{
"type": "order.created",
"id": "evt-44f2",
"timestamp": "2025-04-01T09:12:00Z",
"payload": {
"orderId": "ord-991",
"customerId": "cust-22"
}
}
Avoid embedding derived state (isHighValue, requiresApproval) — that's consumer logic, not event data.
When to use it
Use event-driven architecture when:
- Components have independent lifecycles and must not block each other
- A single action should fan out to multiple consumers
- You're decoupling services that should evolve independently
Avoid it when:
- The flow is synchronous and linear by nature (a simple request-response chain gains nothing)
- Debugging and tracing costs outweigh the decoupling benefit
- Delivery order matters strictly — standard event buses give no ordering guarantees
Event-driven design trades simplicity for flexibility. Apply it at the seams between components, not everywhere.