The Saga Pattern: Managing Distributed Transactions Without Two-Phase Commit
Once an order touches a payments service, an inventory service, and a shipping service, one database transaction can't cover all of it. The saga pattern accepts that and gives you a way to undo partial work instead.
Published July 6, 2026A traditional database transaction gives you atomicity: either every statement in it commits, or none do. That guarantee is easy inside one database, and hard to extend across several independent services each with their own database, because there's no single lock manager that can hold everything consistent while it decides. The article on two-phase commit covers one answer — a coordinator that gets every participant to agree before anything commits. It works, but it requires a central coordinator, blocks all participants while the vote is in progress, and falls apart badly if the coordinator crashes mid-vote. The saga pattern takes a different approach: give up on atomicity as a single event, and instead define a sequence of local transactions, each committing independently, with an explicit compensating action for each step in case a later step fails.
The basic shape
Take an order placement flow: reserve inventory, charge the customer's card, schedule a shipment. As a saga, each step is its own local transaction against its own service's database:
- T1: Inventory service reserves the items (local commit)
- T2: Payment service charges the card (local commit)
- T3: Shipping service schedules a pickup (local commit)
If T3 fails — the shipping service is down, or there's no carrier available for the address — the saga doesn't roll back a distributed transaction, because there never was one. Instead it runs compensating transactions for everything that already committed, in reverse order: refund the charge (C2), release the reserved inventory (C1). Each compensating action is itself a normal local transaction against that service's own database, using the same mechanisms the service already has for refunds and inventory releases. Nothing about the pattern requires new distributed-transaction machinery; it requires each service to expose a "please undo this" operation alongside its normal "do this" operation.
Choreography vs orchestration
There are two common ways to coordinate the steps. In a choreographed saga, each service listens for events from the previous step and publishes its own event when it finishes, with no central coordinator — this fits naturally with an event-driven architecture built on a message bus. Inventory publishes "items reserved," payment listens for that event and attempts the charge, and so on. It's decentralized and each service only needs to know about the events immediately before and after it in the flow, which keeps coupling low for simple sagas.
In an orchestrated saga, a dedicated orchestrator component calls each service directly and tracks the saga's state explicitly:
class OrderSaga {
async run(order) {
const reservation = await inventory.reserve(order.items);
try {
const charge = await payments.charge(order.card, order.total);
try {
await shipping.schedule(order.address, order.items);
} catch (err) {
await payments.refund(charge.id);
await inventory.release(reservation.id);
throw err;
}
} catch (err) {
await inventory.release(reservation.id);
throw err;
}
}
}
Orchestration makes the flow easier to follow and debug — the whole sequence lives in one place instead of being implied by which service reacts to which event — but it also means the orchestrator itself becomes a piece of infrastructure that has to be reliable and needs its own state persisted somewhere, since a saga can span minutes or hours and the orchestrator might restart mid-flow. Most teams start with choreography for two or three steps and move to orchestration once the number of services and failure branches makes the implicit event chain hard to reason about.
The part that actually requires design work
Writing the "happy path" local transactions is usually the easy half. The hard half is designing compensating transactions that are actually safe to run, including safe to run twice if a retry happens after a timeout. A refund needs to be idempotent — charging a refund twice because a network call timed out and got retried is a real bug, not a theoretical one. Compensations also aren't always a clean mirror of the original action: you can release a reservation that hasn't shipped yet, but you can't un-ship a package that already left the warehouse, so a saga design has to account for a point past which some steps become irreversible and need a different kind of handling (a manual return process, a customer-service escalation) rather than an automatic compensation.
When it's worth the complexity
Sagas are the right tool once a business operation genuinely spans multiple services with independent databases and you can't put everything behind one transaction boundary — which is most non-trivial microservice systems. They're overkill inside a single service with one database, where a normal ACID transaction is simpler and gives you a stronger guarantee for free. The trade-off to accept going in: a saga gives you eventual consistency, not immediate consistency. There's a window, however brief, where inventory shows items reserved but payment hasn't charged yet, and your system has to be built to tolerate other reads seeing that intermediate state. Microsoft's Azure Architecture Center has a detailed writeup of the pattern with sequence diagrams for both coordination styles that's worth reading before implementing your first saga.