The Saga Pattern Explained: Distributed Transactions Without Locks
The saga pattern coordinates a multi-step transaction across services using local commits and compensating actions instead of a distributed lock.
The saga pattern is a way to keep data consistent across a multi-step operation that spans several services or databases, without holding a single distributed lock across all of them for the duration. Instead of one all-or-nothing transaction, a saga breaks the operation into a sequence of local transactions, each committed independently, with a matching compensating action defined for every step in case something later in the chain fails.
The problem it solves
In a single database, a multi-step operation — debit one row, credit another — is wrapped in a transaction and committed atomically: either both changes land, or neither does. That guarantee gets much harder once the steps span separate services, each with its own database. There’s no single engine that can hold locks across all of them and roll everything back atomically on failure.
The traditional distributed answer is two-phase commit, which does provide atomicity across services, but at a cost: every participant holds locks on its resources from the “prepare” phase until every other participant has also agreed to commit. If one service is slow or unreachable, every other participant sits blocked, holding locks, until it resolves — a coordination cost that scales poorly with more services and doesn’t tolerate a coordinator failure gracefully.
A saga trades that atomicity guarantee for availability: each step commits locally and immediately, with no cross-service lock held while waiting on the rest of the chain.
How a saga runs
Consider booking a trip: reserve a flight, reserve a hotel, charge a card. As a saga:
- Reserve flight — commits locally in the flights service.
- Reserve hotel — commits locally in the hotels service.
- Charge card — commits locally in the payments service.
If step 3 fails — the card is declined — the saga doesn’t try to roll back a shared transaction, because there isn’t one. Instead, it runs compensating actions for every step that already committed, in reverse order: cancel the hotel reservation, then cancel the flight reservation. Each compensating action is itself a normal local operation against the service that owns that data, not a rollback in the database sense.
This means every step in a saga needs a defined compensating action before the saga is designed to run, and that action needs to be safe to execute even if the original step partially succeeded — which is why compensations are usually written to be idempotent, the same requirement covered in our piece on idempotency.
Choreography vs orchestration
Sagas are coordinated one of two ways:
Choreography — each service listens for events from the others and reacts on its own, with no central coordinator. The flights service reserves a seat and publishes a FlightReserved event; the hotels service is listening for that event and reserves a room in response, publishing its own event in turn. This keeps services decoupled but makes the overall flow harder to see in one place — tracing what happened requires following events across every service’s logs. This pattern often rides on the same message queue infrastructure used for other asynchronous service communication.
Orchestration — a central orchestrator explicitly calls each service in sequence and is responsible for triggering compensating actions if a step fails. This makes the flow easy to read in one place — it’s just a state machine — at the cost of a coordinating component that every service now depends on.
| Choreography | Orchestration | |
|---|---|---|
| Coordination | Distributed via events | Centralized coordinator |
| Coupling | Looser between services | Services coupled to orchestrator |
| Visibility of the flow | Spread across services | Single place to read |
| Failure handling | Each service reacts to failure events | Orchestrator triggers compensations |
| Best for | Few steps, simple reactions | Longer, more complex workflows |
What a saga does not give you
A saga is not equivalent to a single ACID transaction, and it’s important to be explicit about what’s given up. Between steps, the system is in an intermediate state that’s visible to the rest of the world — after the flight is reserved but before the payment succeeds, a concurrent process reading the flights service sees a reservation that might still get cancelled. This is a form of the eventual consistency described in our ACID transactions and eventual consistency articles, applied at the scale of a whole business process rather than a single database write.
Compensating actions also aren’t always a perfect undo. Cancelling a hotel reservation might incur a fee, or a payment refund might take days to actually clear even though the compensating action registers instantly. Saga design has to account for this by making the “semantic” undo acceptable to the business, not just technically correct.
When to reach for a saga
Sagas earn their complexity in systems already built as independent services with independent databases — the kind of microservices architecture where a shared distributed transaction across service boundaries isn’t realistically on the table. For a monolith with one database, a normal local transaction is simpler and gives stronger guarantees for free; introducing sagas there would be solving a problem that doesn’t exist yet.
The takeaway
A saga replaces one distributed transaction with a sequence of local transactions plus a compensating action for each, trading strict atomicity for availability and loose coupling between services. Choreography keeps services decoupled but spreads the flow across event logs; orchestration centralizes the flow at the cost of a shared coordinator. Either way, the tradeoff is the same: an intermediate, partially-completed state becomes visible for a while, and compensating actions have to be designed as acceptable business outcomes, not just as a technical undo.
Keep reading
The Lycoris Team · · 5 min read The Raft Consensus Algorithm, Explained
Raft is a consensus algorithm that lets a cluster of servers agree on a shared state even when some nodes fail. How leader election and log replication work.
The Lycoris Team · · 5 min read What Is Two-Phase Commit (2PC)? Distributed Transactions
Two-phase commit coordinates a transaction across multiple databases with a prepare phase and a commit phase, trading availability for strong consistency.
Chisato · · 4 min read What Is Eventual Consistency in Distributed Systems?
Eventual consistency guarantees that replicas converge over time, not instantly. How it differs from strong consistency and when it's acceptable.