Architecture patterns
Multi-step consistency
When two systems must agree, such as inventory and payment, use a saga with compensating steps, an outbox for events, idempotency keys on every hop, and a reconciliation job. Two-phase commit everywhere is rarely the answer.
A single business action often touches two systems. Inventory and payment must agree. A message and its push notification must agree. A ledger and an external payment provider must agree. You cannot wrap both in one transaction, so you need a pattern that survives a crash between the steps.
Concept
Before you start. A saga is a sequence of local steps. Each step has a compensating step that undoes it, if a later step fails. An outbox is a table where an event is written in the same transaction as the data it describes, then relayed to a queue. An idempotency key is a stable ID that makes a retried request a no-op. Reconciliation is a job that compares two systems and fixes the drift.
The big idea
Think of booking a trip. You book a flight, then a hotel. If the hotel fails, you cancel the flight. Each booking is a step, and each cancellation is a compensation.
The analogy hides a limit. Some undo steps are not perfect. A refund is not the same as a payment that never happened, and an email that was already sent cannot be unsent.
Walking through it
A ticket purchase runs in four steps. The third one fails.
1. Hold seat 14C ok
2. Authorize the card ok
3. Commit seat 14C fails, sold out elsewhere
4. Void the authorization compensating step, the card is never chargedThe outbox fixes a common bug. A service writes an order to its database, then publishes an event. If it crashes between the two, the order exists but nobody hears about it.
One transaction:
INSERT order row
INSERT outbox row ("OrderPlaced", order 881)
COMMIT
Relay (separate process):
read unsent outbox rows, publish to the queue, mark sentEach hop also takes an idempotency key. A retried payment with the same key returns the first result, and does not charge twice. Finally, a nightly reconciliation job compares orders with payments, and flags any mismatch for a person or an automatic fix.
Common mistakes
- Publishing an event after the database write, not inside the same transaction. A crash between the two loses the event.
- Treating a compensation as a perfect undo. A refund leaves a trace. Design the business rule for that.
- Retrying without an idempotency key. A retry can charge twice.
- Skipping reconciliation. Drift between two systems grows silently until someone notices.
Check yourself
- Why use an outbox instead of publishing right after the database write? A crash between the write and the publish loses the event. Writing the event in the same transaction removes that gap.
- What does a compensating step do in a saga? It undoes an earlier completed step, such as voiding an authorization, instead of rolling back one transaction.
Key takeaways
- A saga runs local steps, each with a compensation for failure.
- An outbox writes events in the same transaction as the data they describe.
- Idempotency keys make every retry safe.
- Reconciliation catches the drift that all of the above can miss.
Further learning
For the trade-offs of full distributed transactions, see distributed transactions.
For the repeat-safe design behind the keys, see idempotency.
Next, see how long-running work and live updates are delivered, in realtime and long-running work.
Related
Distributed transactions
Keeping data consistent when one business operation spans several databases or services, using two-phase commit, sagas, the transactional outbox and reservations, and redrawing boundaries first.
Idempotency and retries
Making operations safe to retry, with idempotency keys, deduplicating consumers and jittered backoff, so at-least-once delivery never means a double charge.
Case study: payment system
Moving money correctly, with payment intents, idempotency at every hop, a double-entry ledger, webhooks and reconciliation when the network lies. Start here for the payments map.