The Problem
one action, several stores, no shared transactionInside one database, a transaction makes several writes all-or-nothing â ACID handles it (Consistency Models). But "place an order" now touches the order service, the payment service, and the inventory service, each with its own database. There is no transaction that spans them. If payment succeeds and inventory fails, you've charged a customer for something you can't ship.
You can't stretch local ACID across the network. So you reach for one of a few patterns that trade the clean guarantee for something workable.
Two-Phase Commit
atomic across nodes, at a steep price2PC keeps strict atomicity with a coordinator and two phases. Prepare: the coordinator asks every participant "can you commit?" and each locks its data and votes yes/no. Commit: if all voted yes, the coordinator tells everyone to commit; if any said no, everyone aborts.
PHASE 1 â prepare PHASE 2 â commit
coordinator ââļ "prepare?" ââļ all yes â coordinator ââļ "commit" ââļ done
ââ payment: yes (locks) any no â coordinator ââļ "abort" ââļ rollback
ââ inventory: yes (locks)
ââ shipping: yes (locks) â participants HOLD LOCKS between phases
â coordinator dies after prepare?
participants block, locked, waiting
Sagas
local transactions plus compensationA saga drops global atomicity and embraces the network. It's a sequence of local transactions, each committing in its own service. If a later step fails, you don't roll back â you run compensating transactions that semantically undo the earlier steps. No locks held across services, so it stays available.
Payment saga â forward steps, and the compensation path on failure:
âļ create order âââļ charge card âââļ reserve stock âââļ â shipping fails
â
â cancel order âââ refund card âââ release stock ââââââââ
(each compensation is a NEW local transaction, not a rollback)
Each service listens for events and emits its own. No coordinator â the flow emerges from who reacts to what. Simple for short sagas; hard to follow and debug as steps multiply, because the logic is scattered.
A central orchestrator tells each service what to do and when, and triggers compensations on failure. One place holds the flow, easier to reason about and monitor; the orchestrator is one more component to build and run.
The Outbox Pattern
the default answer to "publish events reliably"Sagas and event-driven designs assume you can reliably publish an event when you change data. But "commit to the DB" and "publish to the broker" are two systems â you can't make both atomic. Crash between them and you've either updated the DB with no event, or published an event for a change that rolled back. This is the dual-write problem.
THE TRAP: two writes, no atomicity
write DB â âââŗ crash ââ publish event â â event lost forever
THE OUTBOX: one local transaction
ââ BEGIN âââââââââââââââââââââââââââââ
â update orders â both commit together,
â insert into OUTBOX (the event row) â atomically â same DB
ââ COMMIT âââââââââââââââââââââââââââââ
â
a relay process reads the outbox table
âŧ
publishes to the broker, marks the row sent
(retries safely â at-least-once)
The fix: write the event into an outbox table in the same local transaction as the data change. Now they commit or fail together. A separate relay polls the outbox and publishes to the broker, marking rows as sent. If the relay crashes mid-publish, it retries â so delivery is at-least-once, and consumers must dedup (Concurrency Control). This is the standard, reliable way to turn a state change into an event.
Change Data Capture
the database as an event sourceCDC tails the database's write-ahead log (Debezium is the common tool) and turns every committed change into an event stream (WAL). Since the log records exactly what committed, downstream consumers â search indexes, caches, analytics â stay in sync with zero application code writing events.
Idempotency Is the Glue
every step must tolerate replayEvery pattern here delivers at-least-once: the outbox relay retries, saga steps re-fire, CDC replays after a restart. So every step must be safe to run twice. Idempotency isn't an add-on â it's the property that makes the whole approach correct. A retried "charge card" that charges twice turns a resilience feature into a bug.
Quick Reference
2PC vs saga vs outbox| 2PC | Saga | Outbox | |
|---|---|---|---|
| Consistency | Strong, atomic | Eventual | Eventual (reliable publish) |
| Latency | High (locks, round trips) | Low per step | Low |
| Failure behavior | Blocks on coordinator | Compensate backward | Retry the relay |
| Complexity | Coordinator, locks | Compensations to design | Outbox table + relay |
| Use when | Rare â true atomicity required | Cross-service business flows | Reliably publishing events |