systemref

Event-Driven Architecture — A Primer

H. Maqsood Jun 8, 2026 2 min read architecture async messaging systems event-driven
Event-driven architecture decouples producers from consumers through a shared event contract. Components react to state changes rather than calling each other directly.

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.