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:
- Schema evolution. A five-year-old
OrderPlacedwith nocurrencyfield must still replay. You version events and write upcasters, and you keep them forever. - Replay time. A million-event stream is not replayed per request. Snapshots every N events bound it — and are a second thing to version and invalidate.
- No UPDATE, no DELETE. Fixing bad data means appending a correcting event, and GDPR erasure is the sharp edge: you cannot delete from an append-only log. The answer is crypto-shredding — encrypt personal data under a per-subject key held outside the log, then destroy the key.
- Cognitive load. Most tooling assumes rows, and every engineer has to learn the model.
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
- Commands are rejectable instructions to one handler; events are immutable past-tense facts to many.
- Choreography is loosely coupled and untraceable; orchestration is visible and testable, with a component that must not fail.
- CQRS pays off when read and write shapes or scales diverge; the price is staleness and a read model you can rebuild.
- Event-source only where the audit trail is the product; schema evolution, replay cost and GDPR erasure are permanent taxes.
- CQRS and event sourcing are independent choices, and conflating them is the classic mistake.
- Sagas trade isolation for availability: compensate with business actions, irreversible steps last.
Check yourself
-
A team publishes a message named ReserveStock to a topic that several services subscribe to. What is the core problem?
ReserveStock is imperative: it instructs one handler and can be rejected. Broadcast it and every subscriber races to reserve the same stock. The tempting fan-out answer is wrong for the opposite reason — fan-out works fine, which is exactly what makes broadcasting a command harmful.
-
Which statement about CQRS and event sourcing is correct?
You can run CQRS against an ordinary relational write model that publishes change events, and you can event-source one aggregate read by replaying its own stream. The tempting claim that event sourcing requires CQRS is wrong because replaying a stream to rebuild an aggregate is a normal read path.
-
You must erase a user's personal data, but your source of truth is an append-only event log. What is the standard approach?
Crypto-shredding leaves the log append-only while making the personal data permanently undecryptable. Appending a UserErased event is tempting because it fits the model, but the original data still sits in the log in plaintext and any replay or backup restore exposes it.
-
A saga charges a card, sends a shipping confirmation email, then creates the shipment — and the shipment fails. What does this reveal?
A delivered email cannot be undone, so irreversible steps belong after every step that can still fail. The rollback answer is wrong because sagas have no rollback by definition — a refund is precisely the correct compensating action for a charge.