The saga pattern: an interactive compensating transactions simulator
A saga is a business transaction that spans several services, split into a sequence of local transactions, one per service, each committed on its own. If a later step fails, the saga runs compensating actions that undo the earlier steps, newest first. It gives you all-or-nothing in the end without locking anything across services, at the price of intermediate states that others can see.
The playground books a trip across four services: flights, hotels, car rental and payments. Make a step fail, turn compensation off, crash the orchestrator, or make a compensation time out, and watch the log.
No car is available, so the flight and the hotel, already committed in their own services, are undone by compensating actions, newest first. For a while other customers could see what was reserved as taken: a saga gives up isolation.
- OrchestratorStarts saga trip-8812, writing each step to its log.
- FlightsReserve seat 14C: done, committed locally.
- HotelsBook 3 nights: done, committed locally.
- CarsRent a car: fails (none available).
- OrchestratorStep 3 failed: undoes steps 2 and 1, newest first.
- HotelsCancel the booking: done.
- FlightsRelease seat 14C: done.
- OrchestratorEvery completed step has been undone: the trip is cancelled cleanly and the customer is told.
How a saga works
- Split the transaction into steps that each change one service, and give each a compensating action: reserve and release, charge and refund.
- Run the steps in order, each committing locally. Nothing is held locked while the next step runs.
- On a failure, compensate: run the undo of every completed step, in reverse order.
- Record progress durably, so that a crash in the middle resumes the saga instead of abandoning it.
Orchestration vs choreography
| Orchestration | Choreography | |
|---|---|---|
| Who decides the next step | A central orchestrator calls each service | Each service reacts to the previous one’s event |
| Where the flow lives | In one place, easy to read and change | Spread across services |
| Coupling | Orchestrator knows every service | Services know only events |
| Debugging | One log shows where a saga is | Needs tracing across services |
| Good for | Long or branching flows, timeouts, compensation | Short, linear flows with few steps |
Workflow engines such as Temporal, AWS Step Functions and Camunda are orchestrators with the durable log built in, which is most of the hard part.
What makes sagas hard
- Durable progress: an orchestrator that keeps its place only in memory forgets the saga when it crashes, and the seat and room stay reserved for a trip nobody will finish or cancel. Store the saga’s state, or use a workflow engine that does.
- Retryable, idempotent steps: after a crash, a step may run twice; after a timeout, the orchestrator cannot know whether it happened. Every step and every compensation needs an idempotency key.
- Compensations that cannot fail: there is nothing to undo an undo, so compensations are retried until they succeed, and must be designed so they eventually can.
- No isolation: between booking and cancelling, other customers see the room as taken; a report run mid-saga sees half a trip. Use pending states (“reserved, not confirmed”), and order steps so the ones most likely to fail come first and the irreversible ones, like sending an email or charging a card, come last.
- Not everything can be undone: an email sent stays sent. Compensations are often semantic: a refund rather than an uncharge, a cancellation notice rather than an unsend.
A saga orchestrator in code
STEPS = [
(flights.reserve, flights.release),
(hotels.book, hotels.cancel),
(cars.rent, cars.cancel),
(payments.charge, payments.refund),
]
def run_saga(saga_id, trip):
log = SagaLog.load(saga_id) # durable: resume after a crash
for i, (do, undo) in enumerate(STEPS):
if log.done(i):
continue # finished before the crash
try:
do(trip, idempotency_key=f"{saga_id}:{i}")
log.mark_done(i)
except PermanentFailure:
for j in reversed(range(i)): # undo what was done, newest first
retry_forever(lambda: STEPS[j][1](trip, idempotency_key=f"{saga_id}:undo:{j}"))
log.mark_compensated(j)
return "cancelled"
return "booked"In a system design interview
When an operation spans services with their own databases, name the saga and its compensations: “Checkout is a saga run by a workflow engine: reserve stock, authorise payment, create the shipment, then capture the payment. If shipping fails, we void the authorisation and release the stock. Each call carries an idempotency key, so retries after a crash are safe, and the order shows as pending until the saga finishes.” Expect the follow-ups: why not two-phase commit, what users see mid-saga, and what happens when a compensation keeps failing.
Read the guide to distributed transactions and idempotency, compare with two-phase commit, see the transactional outbox for choreographed sagas, or watch a strong candidate design a workflow engine.
Questions people ask
What is the saga pattern?
A way to run a transaction that spans several services, each with its own database: the work is split into a sequence of local transactions, each committed on its own, and every step has a compensating action. If a later step fails, the compensations of the completed steps run in reverse order, so the overall result is all or nothing.
What is a compensating transaction?
An action that semantically undoes a completed step: releasing a reserved seat, cancelling a booking, refunding a charge. It is a new transaction, not a rollback, so the original change was visible in the meantime, and it must be idempotent and retried until it succeeds, because nothing can undo a failed undo.
What is the difference between orchestration and choreography in sagas?
In orchestration a central coordinator calls each service in turn and decides what happens next, including compensation, which keeps the flow in one place. In choreography each service listens for events and emits its own, with no central coordinator, which reduces coupling but spreads the flow across services and makes it harder to follow and change.
What is the difference between a saga and two-phase commit?
Two-phase commit locks resources in every participant until all agree, giving atomicity and isolation but blocking if the coordinator fails. A saga commits each step immediately and undoes steps with compensations if needed, so nothing is locked across services, but intermediate states are visible and compensations must be designed for every step.
How do sagas handle failures of the orchestrator?
The orchestrator records each completed step in durable storage before moving on, so after a crash it reads its log and resumes, either continuing forward or compensating. Because a step may have run just before the crash without being recorded, every step must be idempotent. Workflow engines like Temporal and Step Functions provide this durability.
What isolation problems do sagas have?
Other transactions can see a saga’s intermediate state, such as a room booked for a trip that will be cancelled a moment later, and can make decisions on it. Common countermeasures are pending or reserved states, ordering steps so likely failures come first, re-checking values before acting, and placing irreversible steps last.
When should you use a saga?
When one business operation must update data owned by several services or external systems, such as booking travel, placing an order or onboarding a customer, and you cannot or should not hold locks across them. If all the data lives in one database, use a local transaction instead; it is far simpler.