Sagas
A Socratic walk-through of sagas — reasoned out one step at a time, not lectured.
The question we started with
THE QUESTION #How do you undo half a transaction that spanned five systems, none of which can roll back?
Inside one database, a failed transaction is a non-event. The database was writing to a log the whole time, nothing was visible to anyone else, and rolling back means discarding work nobody ever saw.
Now spread the same operation across five services. Reserve the seat, charge the card, book the hotel, issue the ticket, send the confirmation. The card is charged; the ticket printer is down. What does "roll back" mean now? The payment processor has already moved money and told its own systems so. There is no log to rewind, no lock still held, and no authority that could rewind it if there were.
So the question is not how to roll back. It is what you do instead, once you accept that rolling back is not available.
Reasoning it through
REASONING #First, be clear about why the classic answer is unavailable, because it exists and people reach for it. A distributed transaction using two-phase commit really can make five systems commit or abort together: a coordinator asks everyone to prepare, and only when all agree does it tell them to commit. But look at what "prepare" requires — each participant holds its locks and waits for the coordinator, so a coordinator that dies at the wrong instant strands them holding resources with nobody to tell them what to do. That blocking problem is inherent, not an implementation flaw. It also demands every participant speak the protocol, which a third-party payment API does not.
Given that, what is actually possible? Each of the five steps can commit locally, immediately and independently. What we lack is a way to make step three's failure undo steps one and two.
But is undoing them impossible, or only impossible in the database's sense? Consider the charge. There is no rollback — but there is a refund. There is no un-reserve — but there is a cancellation. Each is a new, ordinary, forward transaction whose business effect approximately reverses an earlier one. That is the key move, and it is worth stating as sharply as possible: you cannot erase what happened, so you do something new that makes the world acceptable again.
That structure — a sequence of local transactions, each with a compensating transaction that semantically undoes it — is a saga. The name and the idea come from a 1987 paper by Hector Garcia-Molina and Kenneth Salem, written about long-lived transactions within a single database, and adopted decades later for distributed services because the shape fits.
So the execution model is: run steps forward; on failure at step n, run the compensations for steps n-1 down to 1, in reverse order. Reverse order matters — releasing a seat before refunding the charge can leave a payment attached to nothing.
Who drives that? In orchestration, one component holds the workflow, calls each service in turn and issues compensations when needed: the logic is in one readable place, at the price of a component that knows about everybody. In choreography, each service reacts to events the others publish, with no central brain — nothing is coupled to a coordinator, but the workflow exists nowhere explicitly, and understanding a failure means reconstructing it from five systems' logs.
Now the part that catches people, and the reason sagas are harder than they look. A saga gives up isolation. Between step one and step five, the outside world can see a half-completed state: the seat is reserved and the card is charged, but no ticket exists. Another transaction can read it. Another transaction can act on it. Databases call this a dirty read, and the database prevents it — a saga cannot, because each step really has committed. Sagas are sometimes described as ACD rather than ACID for precisely this reason.
That has consequences you design for rather than discover. What if someone else acts on the half-finished state? What if compensation is not fully possible — the email has gone, the goods have shipped, a non-refundable fee was taken? Chris Richardson's catalogue of countermeasures is the practical answer: mark records as provisional (a semantic lock), design updates whose order does not matter, or re-read a value before acting on it. Choose one deliberately, or you get a system correct in the happy path and quietly wrong in the interleavings.
Two operational requirements complete the picture. Every step and every compensation must be idempotent, because a saga coordinator that crashes mid-flight will retry, and it cannot know whether the previous attempt landed. And compensations must be retried until they succeed, since a failed compensation leaves exactly the inconsistent state the saga exists to prevent — which is why serious implementations persist saga state durably and alert a human when a compensation is stuck.
The analogy
THE ANALOGY #Think of a written cheque rather than a cash payment. Cash is a database transaction — the handover is atomic and instant. A cheque is a saga step: it commits immediately in the sense that it is out of your hands and the recipient is acting on it, and if you change your mind you cannot un-write it. You issue a stop instruction, which is a new action taken later, and it works only if you are quick enough, and it does not undo the dinner the recipient already bought on the strength of it.
a stopped cheque usually leaves no trace, whereas a saga's compensations are permanent entries in the record — the refund sits beside the charge forever — so the honest statement is that a saga restores the balance of the world, never its history.
Clarifying the model
THE MODEL #Three refinements.
A compensation is not an inverse. It is a business action chosen to make the outcome acceptable, and it usually leaves visible residue: a refund with a fee, a cancellation notice, an audit entry. Designing one means asking "what would we do about this in the real world?", which is a domain question, not a technical one — and it is why the compensation for "send the confirmation email" is usually "send a correction", not "unsend".
Some steps cannot be compensated at all, and the design response is ordering: put the irreversible ones last, once everything reversible has succeeded.
Finally, this is not a lesser substitute for a distributed transaction. It is a different bargain: you get availability and independence — each service commits alone, no coordinator can block anyone, third parties need support nothing — and you pay in isolation, which you buy back explicitly wherever it matters. Understanding that trade is what stops a saga becoming a distributed transaction badly reimplemented.
A picture of it
THE PICTURE #How to readRead downward through the three forward states — each commits by itself, with no coordinator holding it open. Then see where the failure arrows lead: not backwards along the path taken but sideways into named compensating states, in reverse order of the work done, so the money returns before the seat is released. Both exits reach the same terminal, which is the saga's promise. What the picture cannot show is the interval: while the machine sits mid-path, everyone else can see a charged card with no ticket.
What became clearer
WHAT CLEARED #A saga starts from an admission: across independent services there is nothing to roll back, because every step has genuinely committed. What replaces rollback is a deliberately designed reverse path made of ordinary forward transactions — refunds, cancellations, corrections — run in reverse order and retried until they land. The cost is isolation. For a window, the world can see and act on a half-finished operation, so the real work of designing a saga is not the happy path but deciding what everyone else is allowed to see and do while it is still running.
Where to go next
ONWARD #- The countermeasures for lost isolation, and when each is worth its complexity.
- The transactional outbox, which makes "commit locally and publish an event" reliable rather than best-effort.
Key terms
TERMS #| Term | What it means |
|---|---|
| Saga | a sequence of local transactions across services, each paired with a compensating transaction that semantically undoes it. |
Every term the collection defines is gathered in the glossary.