Tier 3 · Patterns & Assembly

Event-Driven Architecture

Events, commands, CQRS, event sourcing and sagas — and which of them you actually need

⏱ 19 min patternseventscqrs

Commands and events

A command is a request to do something. PlaceOrder, ReserveStock. It goes to one handler, it expresses intent, and it can be rejected — card declined, stock gone, no permission.

An event is a statement that something happened. OrderPlaced, StockReserved. Past tense, immutable, zero or many consumers, and it cannot be rejected. A consumer can ignore an event or compensate for it, but cannot make the fact untrue.

Naming discipline is not cosmetic. PlaceOrder on a topic is a command in an event's costume: the publisher tells somebody to act while pretending to announce a fact. The moment a second consumer subscribes, two services race to perform one action. Publish OrderPlaced and each consumer decides what it means for itself.

Choreography and orchestration

Two ways to run a multi-step process over events.

Choreography — each service subscribes to the events it cares about and publishes its own. Checkout emits OrderPlaced; payment reacts and emits PaymentCaptured; shipping reacts to that. Nobody coordinates. Adding loyalty points is one new subscriber and nothing else.

Orchestration — a coordinator (an order service, a workflow engine like Temporal or Step Functions) holds the process definition and issues commands in sequence. The flow lives in one readable function.

Choreography Orchestration
Flow definition Emergent, in no single place Explicit, in one component
Coupling Publishers know nothing of consumers Coordinator knows every participant
Debugging Reconstruct from traces Read the state machine
Testing the flow Only end-to-end Unit-testable against fakes
Best when Independent reactions Ordered steps with compensation

Choreography is right when a fact has independent consequences: UserSignedUp triggering a welcome email, an analytics record and a CRM sync — unrelated reactions that should not share a coordinator. Orchestration is right when the steps form one transaction with ordering and rollback: payment, stock, fulfilment.

The hook is what choreography failure looks like: correct in every service, wrong overall, and finding out why was archaeology. Tracing (observability) makes that survivable, not pleasant. The central-component objection is overstated: a coordinator that persists state after each step restarts cleanly, and its failure mode is a stalled workflow.

CQRS

Command Query Responsibility Segregation: one model to write, another to read. The write side owns invariants; the read side is a denormalised projection fed by the write side's events.

It earns its cost when the shapes genuinely diverge — a write model enforcing per-line-item rules against a read model serving one flat document per customer, no joins — or when they scale differently: 50,000 reads/sec against 200 writes/sec is not one workload, and the read side may want another engine (search or a column store).

The costs are specific. Eventual consistency: the user writes, lands on a page reading the projection, and does not see her own change. Fix it deliberately — read the write model after a write, return the new state in the response, or pin the session until the projection catches up (consistency models). Rebuildability: projections get corrupted by bugs, and if you cannot drop one and rebuild it from the log you have no repair. Plus two schemas, two deployments and a lag metric to alarm on.

Event sourcing

Store the sequence of events as the source of truth and derive current state by replay. Instead of a row saying balance = 90: AccountOpened, Deposited(100), Withdrew(10).

The benefits are real: an audit trail that cannot drift from the data because it is the data; time travel, making "what did we show her at 14:05?" answerable; and new projections over history, so a report invented today is computable across three years.

What it costs:

Sagas

A saga is a distributed transaction split into local transactions, each with a compensating action. Reserve stock → charge card → create shipment; if the shipment fails, refund the charge, then release the stock. It exists because 2PC blocks every participant while the coordinator decides, and a coordinator crash holds those locks indefinitely — fatal across separately owned services.

The property you lose is isolation. Each local transaction commits immediately, so intermediate states are visible: stock sits reserved for an order cancelled two seconds later, and concurrent queries see it. Handle that with semantic locks — an order status of PENDING that other reads distrust.

Compensation is a business concept, not a rollback. You do not un-charge a card; you issue a refund, a new transaction with its own record and its own fee. You do not un-ship stock; you accept a return. Compensations appear on statements and in support calls, so they must be things the business already does.

Some steps cannot be compensated — an email has been read, a payout has cleared. Ordering matters: do the reversible steps first and the irreversible ones last, and make irreversible steps provisional where you can — authorise the card early, capture only once everything else succeeds.

Sagas demand delivery discipline. Committing locally and publishing the event must be atomic, or you announce something that did not happen — use the outbox pattern: write the event to an outbox table in the same transaction and let a relay publish it. Consumers must be idempotent because the relay retries (idempotency), and replay rests on the log semantics in queues and logs.

What to take away

Check yourself

  1. A team publishes a message named ReserveStock to a topic that several services subscribe to. What is the core problem?

  2. Which statement about CQRS and event sourcing is correct?

  3. You must erase a user's personal data, but your source of truth is an append-only event log. What is the standard approach?

  4. A saga charges a card, sends a shipping confirmation email, then creates the shipment — and the shipment fails. What does this reveal?