The Saga Pattern: Distributed Transactions Without Two-Phase Commit
The saga pattern explained: long-lived business transactions as local steps with compensating transactions, orchestration vs choreography, the transactional outbox that fixes the dual write, and the 2pc vs saga decision.
The saga pattern is a way to run a long-lived business transaction across services as a sequence of local transactions (each atomic within one service’s own database) arranged so that the sequence either completes or is undone step by step with compensating transactions. It replaces distributed atomicity with eventual completion, and it is the standard answer when the work spans machines and minutes.
The boundary with its prerequisite stays as distributed transactions drew it: that article owns two-phase commit and its alternatives’ mechanics (the coordinator, the voting rounds, why the design blocks) and this article owns what ships when that instant cannot be had: the saga pattern itself, orchestration vs choreography, the outbox pattern, and the 2pc-vs-saga decision. Event-driven architecture keeps the everyday choreography and event sourcing; where its event store and state changes must agree atomically, it borrows this article’s outbox rather than re-deriving it.
What is the saga pattern
The pattern is older than the architecture that made it famous. The term comes from a 1987 paper by Hector Garcia-Molina and Kenneth Salem, which studied transactions that stay open for minutes or hours and proposed splitting them into sequences of short ACID transactions, each paired with a compensating transaction that semantically undoes it. Microservices architecture recreated that problem between databases instead of inside one: with database-per-service, placing an order touches payments, inventory, and shipping as three autonomous systems with three transaction managers, and no lock manager or commit protocol spans them. The saga pattern for microservices is the same move the paper made: stop trying to make the long transaction atomic, and make each of its pieces atomic instead.
A saga is the sequence those pieces form: local transactions, each a real commit in exactly one database, each with a compensating transaction designed to undo it. The rule of operation is simple: run forward until a step fails or the business says stop; then undo the completed steps in reverse order. The guarantee the pattern sells is not atomicity; it is eventual completion: given enough time and enough retries, the saga ends with every step done, or with every completed step compensated. Between those ends, the transaction is observably mid-flight, and everyone involved has to be able to live with that.
Two properties of the all-or-nothing instant are surrendered, and both are the designer’s to buy back. The first is isolation: intermediate states are real and readable. An order can be paid but not yet reserved, and a query that runs in that window sees exactly that, 2PC would have hidden it behind locks; a saga cannot hide it without becoming 2PC. The countermeasures are design work, not protocol: semantic locks that mark a row as in-progress, commutative updates whose order does not matter, read paths that tolerate in-flight state. The second is automatic rollback: undoing work stops being the database’s job and becomes the business’s. A compensating transaction is a business action (a refund, a release, a cancellation) designed and tested alongside the step it undoes, and safe to retry, because anything that can be redelivered will be, and anything retried must be idempotent.
Saga steps and compensations
Each step is two acts in one local transaction: change the state, and record the handoff. The commit carries both the business change and the message that moves the saga forward (a command aimed at the next participant, or an event published for whoever is listening) written atomically by the transactional outbox, which a section below owns. The step boundary is also the failure boundary: a crashed process leaves the local transaction either committed or not, and the saga’s state says exactly which step it was on when the lights went out. Handoffs are messages, and messages get redelivered (at-least-once delivery is the pipeline default) so every step, and every compensation, is built to receive the same instruction twice and do the work once.
Compensation design is where sagas earn or lose their reputation. Four properties: a compensation is a business-meaningful undo (refund the charge, release the reservation, cancel the shipment) not the inverse SQL; it is idempotent and retriable, because it will be re-attempted after crashes and redeliveries; it is state-tolerant; the record it compensates may have moved on since the forward step ran; and it is designed with its forward step, because “how would we undo this?” is a requirement of the step, not an afterthought. The sequence also earns a pivot: up to the pivot, steps are compensatable; past it, the business has physically happened (the package has left the building) and a failure is handled as new forward work, a return or a claim, rather than as compensation.
A short worked example: an order saga charges the payment, reserves inventory, books the shipment, and confirms the order. The carrier API refuses at step three. The saga runs backward: the inventory reservation is released, the charge is refunded, the order is marked cancelled; each undo a local transaction in its own database, each visible to the customer as exactly what it is. At four steps this reads as bookkeeping; at twenty steps, across five services, with the failure arriving on a Friday night, it reads as the difference between an outage and a routine.
The case the happy-path diagram skips: the refund call fails, and keeps failing. Compensations are never allowed to be lost; they are journaled with the saga’s state and retried, paced like any retry with backoff and jitter, until they land or until the escalation rule fires: park the saga for a human, quarantine the affected records, open the reconciliation task. A saga that can sit in “compensation pending” forever is a design with a hole in it; the terminal states (completed, compensated, escalated) are part of the contract, and so is the pager that gets paged when the third one is reached.
Orchestration vs choreography
There are two ways to drive the sequence, and the first decision is that the choice is per flow, not per system. In orchestration, a coordinator (the orchestrator) owns the sequence as an explicit state machine: it sends each command, records each reply, decides the next step, and drives the compensations when something fails. The whole transaction is written down in one place, and that is the model’s entire argument: the flow can be read, tested, versioned, and explained to an auditor, and the compensation order is code, not folklore. The cost is the orchestrator itself; a component to run redundantly and scale, whose state wants journaling the way consensus systems journal theirs, so that a crashed orchestrator is a restart rather than an incident, and, organizationally, a place where every flow change passes through one team’s hands.
In choreography, there is no coordinator: each service performs its step and publishes an event, and whoever cares about that event performs the next one. The flow is emergent (a set of local rules that compose into a transaction) and the coupling is minimal: the shipping service does not know the payment service exists; it knows that “payment succeeded” happens, and what it does about it. This is event-driven architecture in its natural habitat. The costs are the mirror image of orchestration’s strengths: the transaction exists in full nowhere, so reconstructing “what happens when shipment fails” means reading every subscriber of every event; cycles are possible (A reacts to B while B reacts to A) and nothing warns you at design time; and versioning an event is a negotiation with listeners you may not know you have. Visibility has to be built deliberately, with correlation identifiers carried on every event, so one order’s trail through the fleet can be followed at all, the discipline distributed tracing formalizes.
The decision rule that event-driven architecture left here to referee holds up in practice: short cascades with few steps and no money at stake choreograph beautifully; long flows, flows with compensation, and flows someone will have to explain to an auditor want a conductor. Behind the aphorism sit the real drivers: how many steps, whether compensation logic exists at all, who is accountable for the flow’s correctness, and how often the flow changes; an unstable flow rewards the one place that edits it, a stable one rewards the rules nobody has to coordinate. Mixed systems are the norm; choreographed ripples inside an orchestrated spine, orchestrated seams between choreographed domains, and the mistake is choosing globally and then discovering that some flows were silently the other kind all along.
The transactional outbox
Every saga step commits state and emits a message, and the obvious implementation is broken. Commit the database, then publish: the publish can fail, and the saga stalls with a step done but unannounced. Publish, then commit: the commit can fail, and the fleet reacts to a fiction. This is the dual write (two systems, one intended truth, two independent chances to disagree) and the mistake event-driven architecture already named from its own scar tissue: the commit succeeds and the publish dies, and the rest of the fleet never hears what the database swears happened.
The transactional outbox removes the window by refusing to make it two writes. The state change and the outgoing message are rows written in the same local transaction, so the database’s own atomicity (the one property the saga still has locally) carries the pair: either both exist or neither does. A relay ships the outbox rows afterward: a poller that reads the table and publishes what it finds, or a process tailing the database’s transaction log, so publication is exactly as durable as the commit that produced it. Delivery is at-least-once by construction (a relay that cannot confirm a send simply re-reads the row) which pushes the last obligation onto the receiving side, where it belongs: the consumer deduplicates or makes the step idempotent, the same contract every redeliverable pipeline already demands.
For a saga, the outbox is not an optimization; it is the step’s definition. In choreography, the outbox row is the event the next service is waiting on; in orchestration, it is the channel the orchestrator reads progress and replies from. Strip it out and every step boundary becomes a dual write, at which point “eventual completion” holds only while the network never disagrees with the database; a condition no network has ever agreed to. The pattern also closes the event-store question event-driven architecture left open: when an event store and other state changes must agree atomically, the outbox is the mechanism borrowed, and the borrowing runs one direction; this article keeps the mechanics, that one keeps the architecture.
2pc vs saga: when to use each
Distributed transactions owns the mechanics of the protocol this pattern replaces, so the comparison here is the decision, not the derivation. Two-phase commit buys the instant: all participants commit or none does, and the locks taken during voting hold the isolation; no query ever sees a half-committed anything. What it pays for the instant: blocking; a stalled participant holds everyone’s locks, a dead coordinator leaves the fleet waiting on timeout, and the locks themselves, which is why the design works in milliseconds and dies in minutes. A saga buys the opposite: every step commits locally and immediately, nothing waits on a distant vote, and the timescale is whatever the business needs. What it pays: no atomic instant, intermediate states on public display, and compensation logic (designed, coded, tested) for every reversible step.
The trade-off those two prices encode (isolation, failure handling, when each wins) is the question distributed transactions reserved for this page:
| Dimension | Two-phase commit | Saga |
|---|---|---|
| Atomicity model | All-or-nothing in one voting instant | Eventual: every step completes, or every completed step is compensated |
| Isolation | Held by locks through the protocol; no intermediate states visible | Given up; intermediate states visible; countermeasures are design work |
| Failure behavior | Blocks: a stalled participant or coordinator freezes the transaction until timeout | Runs: failure at a step triggers compensation of the completed ones |
| Rollback | Protocol abort (the database undoes the work | Business compensation) code you design, test, and escalate |
| Timescale | Milliseconds; locks make longer untenable | Minutes, hours, days; no locks held across steps |
| Best fit | Short cross-node instants that genuinely need all-or-nothing | Long-lived, cross-service work on human timescales |
Three rules compress the table. Engineer the transaction away before paying for either; a schema that keeps the work on one shard beats any protocol, the first rule distributed transactions already charged. When cross-node atomicity is genuinely needed and the window is short, take 2PC with its eyes open: the blocking is survivable inside one database or a tightly coupled pair. When the work spans services on human timescales, the locks are the wrong tool and the saga is the answer most systems actually ship. The two coexist honestly; one system can run 2PC inside a service boundary and sagas across service boundaries, which is not indecision but the line drawn at the right scale.
FAQ
What happens when a compensation fails?
It is retried, escalated, and never dropped. Compensations are journaled with the saga’s state and paced like any retry (backoff, jitter, a budget) until they land or until the escalation rule fires: the saga parks for a human, the affected records are quarantined, and a reconciliation task opens. A design that lets a compensation sit unexecuted forever has converted an eventual-completion guarantee into an eventual hope, and the terminal states (completed, compensated, escalated) should be part of the saga’s contract from the first design review.
How do I choose between orchestration and choreography?
Per flow, not per system. Few steps, no compensation worth automating, no auditor, teams that want independence: choreograph. Many steps, real compensation logic, an accountable owner, a flow that changes often enough that one place to edit it beats five readers of it: orchestrate. Mixed systems are the norm in practice, and the failure mode is deciding globally: pick a style per flow, write the decision down, and let the two coexist.
Can other transactions see a saga’s intermediate states?
Yes; that is the property given up. An order can be paid but not yet reserved, and concurrent queries see exactly that window. The countermeasures are design work: semantic locks that mark records in-progress so readers treat them accordingly, commutative updates whose order does not matter, and read paths that tolerate in-flight state. If a specific reader genuinely cannot tolerate the window, that flow is a candidate for 2PC instead; the isolation it buys is the thing it is for.
Does every saga need the transactional outbox?
Every reliable one. Without it, each step’s state change and its outgoing message are two writes to two systems with no atomicity between them (the dual write) and the saga’s guarantee rests on the network never disagreeing with the database. The outbox makes the message part of the step’s own local commit and ships it afterward, which converts the step boundary from a hope into a transaction. “Reliable saga without an outbox” is a design that has not yet met its first crash.
When is two-phase commit the better choice?
When the window is short, the participants are few, and the isolation genuinely matters: a transfer inside one database, a booking across a tightly coupled pair. First, engineer the transaction away; a schema that keeps the work on one shard needs no protocol at all. Past that, the rule is timescale: milliseconds belong to 2PC, minutes and beyond belong to the saga, and the systems that run both are drawing the boundary at the right scale, not hedging.
Related articles
- Next read: monolith vs microservices; the decision upstream of this one: whether the fleet that needs sagas should be a fleet at all, and how many cross-service flows the architecture decision creates in the first place.
- distributed transactions; the prerequisite and the counterpart: two-phase commit’s mechanics, why it blocks, and the all-or-nothing instant sagas trade away.
- event-driven architecture; choreography’s home turf: events, pub/sub, event sourcing, and the dual write the outbox exists to fix.
- microservices architecture; the architecture whose database-per-service price this pattern pays, and the ledger the trade-offs are printed on.
- message queues; the delivery-guarantee anchor: at-least-once, dead letters, and the redeliveries that saga steps and compensations must tolerate.
- idempotency; the property that makes every step and every compensation safe to receive twice and execute once.
- distributed locks, the sibling primitive: mutual exclusion for short critical sections, and the honest reason leases cannot span human timescales.