The Saga Pattern: Distributed Transactions Without Distributed Transactions
A single business action spans three services and there's no ACID transaction to save you. The saga pattern trades atomicity for a chain of local transactions and compensations. Here's how it works, when to reach for it, and the guarantees you quietly give up.
The Problem Nobody Warns You About
You split the monolith. Orders, payments, and inventory each got their own service and their own database. Clean boundaries, independent deploys, happy teams.
Then a customer places an order. You need to: reserve inventory, charge their card, and create the order record. In the monolith, that was one database transaction — all three succeed or all three roll back. Now those three writes live in three databases across the network.
What happens when inventory reserves successfully, the card charge goes through, but the order-creation write fails? You've taken the customer's money and locked stock for an order that doesn't exist.
There's no BEGIN TRANSACTION that spans three services. The saga pattern is how you get something useful in its place.
Why You Can't Just Use a Distributed Transaction
The textbook answer is two-phase commit (2PC): a coordinator asks every service to "prepare," and once all say yes, it tells them to "commit." It gives you atomicity across services.
In practice, almost nobody runs 2PC across microservices, and for good reasons:
⚠️ What 2PC actually costs you
During the prepare phase, every participant holds locks and waits for the coordinator's decision. If the coordinator crashes after "prepare" but before "commit," participants are stuck holding locks indefinitely — this is the blocking problem. Throughput collapses under contention, and your availability is now tied to the slowest, least reliable participant.
2PC optimizes for consistency at the cost of availability and latency. Most business systems want the opposite: stay available, keep latency low, and accept that consistency will be eventual. That trade-off is exactly what a saga makes.
What a Saga Actually Is
A saga is a sequence of local transactions. Each step commits to its own database and publishes an event or sends a command that triggers the next step. There is no global lock and no global commit.
The catch: since each local transaction commits immediately, you can't "roll back" once a later step fails. Instead, every step has a matching compensating transaction — an action that semantically undoes it.
A compensating transaction is not a database rollback. It's a new transaction that reverses the business effect of a completed step. You didn't charge the card and undo it — you charged the card, then issued a refund. Both are real, recorded events. The account balance ends up correct, but the history shows both.
Here's the happy path for placing an order:
Each arrow is an event. Each box is a local transaction that already committed by the time the next one starts. Nothing is holding a lock waiting on the others.
What Happens When a Step Fails
Say inventory can't reserve the stock — the item sold out between the customer clicking "buy" and the reservation step running. The saga now runs backward, firing the compensating transaction for every step that already succeeded.
Inventory fails
The reserve-stock step throws. There's nothing to compensate here yet — the failing step didn't commit anything.
Compensate payment
The saga triggers a refund. This is a brand-new transaction in the payment service. The original charge still happened and is still in the ledger — the refund sits next to it.
Compensate order
The order is marked cancelled (not deleted — you want the audit trail). The customer sees "order failed, payment refunded."
The system reaches a consistent state, but notice what you gave up: for a brief window, the customer was charged for an order that ultimately failed. That window is the price of not using 2PC.
🔴 Sagas have no isolation
This is the guarantee people forget. While a saga is mid-flight, other transactions can see its partial results. Another request might read the "pending" order, or see stock that's reserved-then-released. ACID gives you the I in isolation; sagas do not. You have to design for dirty reads — usually with status flags (pending, confirmed, cancelled) so nothing treats an in-progress saga as final.
Two Ways to Coordinate: Choreography vs Orchestration
Once you accept the saga model, the real design decision is who drives the sequence. There are two answers.
Choreography — no central brain. Each service listens for events and reacts. Payment listens for OrderCreated, does its work, and emits PaymentCharged. Inventory listens for that, and so on. The workflow is an emergent property of who-subscribes-to-what.
Orchestration — a central coordinator (the orchestrator) explicitly tells each service what to do and waits for the reply. The workflow lives in one place: the orchestrator's state machine.
The trade-off is about where complexity lives, not whether it exists.
| Dimension | Choreography | Orchestration |
|---|---|---|
| Workflow logic | Scattered across services | Centralized in one orchestrator |
| Coupling | Loose — services only know events | Services coupled to orchestrator |
| Visibility | Hard — no single place shows progress | Easy — orchestrator holds saga state |
| Adding a step | Wire up new event subscriptions | Edit the orchestrator's flow |
| Cyclic risk | Easy to create event loops by accident | Explicit flow prevents cycles |
| Single point of failure | None (fully distributed) | Orchestrator (needs HA) |
The rough rule: choreography for short, stable sagas (2–4 steps that rarely change), orchestration once the workflow gets long or branchy. When a saga has five-plus steps with conditional paths, tracing "why is this order stuck" across a web of event subscriptions becomes miserable. An orchestrator gives you one place to look.
How many steps does your saga have?
The Two Rules That Keep Sagas From Corrupting Data
Sagas run over unreliable networks. Messages get lost, delivered twice, or arrive out of order. Two properties are non-negotiable.
1. Every step must be idempotent
A PaymentCharged event can be delivered twice. If your payment handler charges the card each time it sees the event, retries turn into double charges. Every step needs to detect "I've already processed this" and no-op the second time — usually by tracking a unique saga/message ID.
2. Compensations must always eventually succeed
A compensating transaction cannot be allowed to just fail and give up — that leaves the system permanently inconsistent. Compensations are retried until they succeed, which means they too must be idempotent, and they must be designed to always be possible.
⚠️ Some things can't be compensated
You can refund a charge. You cannot un-send an email, un-ship a package, or un-launch a missile. For irreversible steps, order your saga so they run last, after every step that might fail has already committed. If the irreversible action is early and a later step fails, you're stuck. Sequencing is a design decision, not an afterthought.
When a Saga Is the Right Tool
Sagas are not free. They add compensating logic, idempotency handling, and a whole new class of "partially completed" states to reason about. Reach for them when the alternatives are worse:
- The operation genuinely spans services that own separate databases, and you've accepted eventual consistency. This is the core case.
- Long-running business processes — anything that waits on a human approval, an external provider, or a timer measured in minutes to days. You can't hold a database transaction open that long; a saga persists the state instead.
- You need an audit trail of what happened, including the failures and reversals. Sagas record every step and compensation as real events.
And when to avoid them:
- The data lives in one database. Just use a local ACID transaction. Don't build a saga to coordinate two tables you could wrap in
BEGIN/COMMIT. - You truly need isolation — no other operation may ever observe an intermediate state. Sagas can't give you that. Rethink the boundaries or keep it in one service.
✅ A useful reframing
A saga is a state machine for a business process, where every transition has an "undo." If you can't clearly describe your process as states, transitions, and compensations, you don't understand the workflow well enough to distribute it yet.
Key Takeaways
- A saga replaces one distributed ACID transaction with a sequence of local transactions, each with a compensating transaction to undo its business effect.
- You trade atomicity and isolation for availability and eventual consistency. Partial states are visible mid-flight — design for them with explicit status flags.
- Choreography (event-driven, decentralized) suits short stable flows; orchestration (central coordinator) suits long, branching, or evolving flows where visibility matters.
- Steps and compensations must be idempotent, and compensations must eventually succeed. Put irreversible actions last.
- If your data fits in one database, you don't need a saga — you need a transaction.
References
- Pattern: Saga — microservices.io — Chris Richardson's canonical write-up with both coordination styles
- Sagas (Garcia-Molina & Salem, 1987) — the original paper that introduced the concept for long-lived transactions
- Compensating Transaction pattern — Microsoft — practical guidance on designing compensations