Module 06 · Lesson 04

Event patterns that survive contact with production

Events are a decoupling tool and a debugging tax. Use three patterns, not thirteen.

Advanced 8 min read Updated 2026-11-02

What you will be able to do

  • Tell an event apart from a command and know why the difference matters
  • Design a saga with compensations for a multi-service workflow
  • Version an event schema without breaking consumers
  • Handle poison messages and replay safely

Events, commands, and the difference

EventCommand
TensePast: order_placedImperative: charge_card
OwnerThe producer that owns the factThe caller that wants something done
ConsumersUnknown, manyOne intended handler
On failureConsumer retries; fact stays trueCaller must learn it failed
CouplingProducer knows nothing downstreamCaller knows the handler's contract

The distinction is practical. If a topic is named like a command and consumed by five services, every consumer is implicitly part of the caller's transaction, and nobody can change the workflow without a coordinated deploy. Name facts as facts and keep command channels one-to-one.

Fan-out: one event, many readers

Placing an order should notify inventory, payments, email and analytics. With a log, that is four consumer groups and no producer change. With a queue, it is four queues and a fan-out step that someone must maintain. The log wins on cost of adding a consumer, which is exactly the thing that changes most often.

Backfill is a design requirement
When a new consumer appears, it usually needs history. If retention is seven days, you cannot backfill a year. Keep raw events in cheap object storage if reprocessing is a real requirement, or state honestly that only seven days can be rebuilt.

Sagas: no distributed rollback

A workflow spanning payments, inventory and shipping has no single transaction. A saga runs local transactions in order and, on failure, runs compensating actions that undo the business effect: refund the card, release the reservation. Compensations are not rollbacks — an email cannot be unsent, so order the steps so irreversible actions happen last.

Reserve
Hold inventory with an expiry, so a crash releases it automatically instead of leaking stock forever.
Charge
Capture payment with an idempotency key derived from the order, so retries cannot double-charge.
Confirm
Commit the reservation and mark the order placed. This is the point of no return.
Notify and ship
Side effects that can fail safely after confirmation, retried independently.
Compensate on failure
Refund, release, and record the reason. Every step needs a defined undo, or the saga is incomplete.

Sagas are eventually consistent by construction. Between charge and confirm, a user may see "processing". That state must exist in your API and in your database — a saga without visible intermediate states produces support tickets that look like data loss.

Schemas and poisoning

  • Add optional fields only. Consumers ignore unknown fields; they break on renamed or removed ones.
  • Never change a field's meaning. A price that switches from cents to dollars is a silent, expensive corruption.
  • Include a schema version and an event id. Version for routing, id for deduplication.
  • Keep payloads small. Large blobs belong in object storage with a reference in the event.

A poison message is one that always fails — malformed, or referencing a deleted entity. Retrying it forever blocks the partition. Send it to a dead-letter topic after a few attempts with the failure reason attached, alert when the dead-letter rate moves, and provide a replay tool that re-injects fixed messages. Without the replay tool, dead-letter queues are write-only graveyards.

Close the loop in one breath
"Order placed is an event on a log keyed by order id. Payment is a saga with a reservation that expires, an idempotent capture, and a refund as compensation. Consumers are idempotent and dead-letter after three failures with a replay path."

Worth remembering

  • An event states a fact in the past tense; a command requests an action. Mixing them couples services through a topic name.
  • Fan-out is cheap on a log and expensive on a queue you have to copy.
  • A saga is a sequence of local transactions with explicit compensations — there is no distributed rollback.
  • Schema changes are additive or they are breaking; adding an optional field is the only free move.

Checkpoint

Answer all 3 questions to finish the lesson.
1. Which is an event rather than a command?
An event states a fact that already happened. The other three ask a specific handler to do something.
2. In a saga, what replaces a rollback?
Each completed step has an explicit undo — refund, release, cancel. There is no atomic rollback across services.
3. A consumer keeps failing on one malformed message and blocks its partition. What is the right response?
Bounded retries plus a dead-letter topic keeps the partition moving while preserving the failure for inspection and replay.