Home › High-Level Design › Saga Pattern
S

Saga Pattern

The moment you split one database into five services, you lose the one thing that made your life easy: the transaction. No more BEGIN; ... COMMIT; across tables. The Saga pattern is how teams keep a business operation consistent when the data lives in six places and no single lock can hold them together. I have shipped this in an order system that touched payments, inventory, and shipping. It works. It is also one of the easiest things to get subtly wrong, which is exactly why interviewers love it.

High-Level DesignAdvanced11 min read

If you remember five things

The whole pattern in one glance. Everything below is the defense of these five calls.

  1. Local commits, no global lock. A saga is a chain of local transactions that each commit immediately, so partial state is visible and you get eventual consistency, not ACID.
  2. Failure means compensate, not roll back. There is nothing to roll back to. You run compensating transactions in reverse order to make it right.
  3. Orchestration for anything non-trivial. Five steps or real branching, and one readable state machine beats tracing events across five services' logs.
  4. Every step must be idempotent. Buses deliver at least once, so each step and compensation carries an idempotency key or you double-charge.
  5. Know the inconsistency window. Name exactly who sees partial state and for how long. That single answer separates people who read about sagas from people who ran one.

1The core idea#

Hold one mental model and the rest falls out of it.

  • ACID transaction: all or nothing, held under a lock until the end.
  • Saga: a chain of committed steps, each of which already happened, plus a documented way to take each one back.
  • A saga is a sequence of local transactions. Each step updates one service's own database, then triggers the next step.
  • If step 4 fails, you do not roll back the way a database would. You run compensating transactions that undo steps 3, 2, and 1 in reverse.

Why that difference bites harder than it looks.

  • Every step commits immediately and is visible to the rest of the system. There is no global lock.
  • So between step 1 and step 5, the world can see partial state: money is captured but the item has not shipped.
  • That is why sagas give you eventual consistency, not the strict consistency a single database handed you for free.

The travel analogy makes the compensation concrete.

  • You reserve a flight, then a hotel, then a car.
  • If the car fails, you cancel the hotel and cancel the flight.
  • Each cancel is a real operation with its own side effects (maybe a fee), not a magic rewind.
  • That cancel is the compensating transaction, and designing good ones is the whole game.
The one line that reframes everything

A database transaction is a promise the system never showed anyone the half-done state. A saga makes the opposite promise: everyone will see the half-done state, and here is exactly how I clean it up. Say that out loud and the interviewer knows you understand what you traded away.

2A worked example with real numbers#

Make it concrete before the theory piles up. Here is the order saga I actually ran, with the real five steps, the real latencies, and the money that moves. Say a customer buys a $79 pair of headphones.

Step Service Local action Typical latency Compensation
1 Order Create order row, status PENDING 15 ms Mark order CANCELLED
2 Payment Capture $79 on the card 200 ms Refund $79
3 Inventory Reserve 1 unit from a warehouse 40 ms Release the reserved unit
4 Shipping Assign a courier, print a label 350 ms Cancel label, unassign courier
5 Notify Email the confirmation 120 ms Send a cancellation email

The happy path.

  • End to end is about 725 ms.
  • That is the number I quote in an interview, and it is dominated by payment and shipping, the two external calls.

Now walk a failure.

  • Steps 1 and 2 commit: the order exists and $79 is captured.
  • Step 3 hits a warehouse that just sold its last unit, so inventory returns out of stock.
  • The saga compensates in reverse: step 3 reserved nothing, so it has no compensation to run, then step 2 refunds the $79, then step 1 marks the order CANCELLED.
  • The refund call is 200 ms, so from the failure at step 3 the money is released inside roughly 250 ms if the refund succeeds first try.

The number the business actually cares about: the inconsistency window.

  • Between the payment capture finishing (end of step 2) and the refund completing, the customer has been debited $79 for an order they will not get.
  • Happy case: that window is about 250 ms.
  • Bad case: the refund call to the payment provider times out and we retry with backoff, so it can stretch to 30 to 60 seconds.
  • A cron sweeps stuck sagas every minute to catch the ones that fall through.
  • When someone asks "who sees the inconsistency and for how long," this is the answer: the customer, on their card statement, for a fraction of a second normally and up to a minute in the tail.
~250 ms
The inconsistency window, happy pathHow long the customer is debited for an order that failed before the refund lands. It is the number the business judges you on, and it blows out to 30 to 60 seconds whenever the refund call retries. Remember both figures.

Scale it to see why the refund path matters.

  • At 500 orders per second, with a 0.5% inventory-failure rate, that is roughly 2 to 3 refund compensations firing per second.
  • The refund path has to be as reliable as the capture path, because it runs constantly, not rarely.

3How it works#

Two ways to coordinate the steps. You have to know both cold.

Choreography: no central brain.

  • Each service listens for events and reacts by doing its work and publishing the next event.
  • Order publishes OrderCreated. Payment hears it, charges the card, publishes PaymentCaptured. Inventory hears that, reserves stock, publishes StockReserved. And so on.
  • The flow lives in the wiring between services, not in any one place.

Orchestration: a central coordinator owns the flow.

  • The orchestrator tells each service what to do and waits for the reply.
  • It owns the state machine: "Call payment. Got success? Call inventory. Got failure? Call payment-refund."
  • The logic sits in one place you can read top to bottom.

Here is an orchestrated order saga, happy path and one failure:

sequenceDiagram participant O as Orchestrator participant P as Payment participant I as Inventory participant S as Shipping O->>P: Capture payment P-->>O: Payment captured O->>I: Reserve stock I-->>O: Out of stock O->>P: Refund payment P-->>O: Refunded O->>O: Mark order failed
The orchestrator drives every call and sees every reply, so the failure branch (reserve fails, refund fires) lives in one place you can read top to bottom. Notice it talks to Shipping only if Inventory had succeeded.

The orchestrator must persist state after every step. This is not optional.

  • If the coordinator crashes right after payment captured but before it records that fact, on restart it must know exactly where it was so it does not double-charge.
  • The saga state ("we are at step inventory, pending") lives in a durable store, not in memory.

The choreography version of the same flow has no orchestrator box. Each service is a node reacting to messages:

flowchart LR A[Order service] --> B[Payment service] B --> C[Inventory service] C --> D[Shipping service] B --> E[(Event log)] C --> E D --> E
There is no coordinator. Every arrow is an event on a message bus, not a direct call, and the event log is the source of truth for what happened. The flow only exists as the sum of who subscribes to what.

One rule holds for both styles: compensation runs backward.

  • If you captured payment, reserved stock, then failed at shipping, you release the stock first, then refund the payment.
  • Reverse order, every time, because later steps may depend on earlier ones.
Pause before you read on: the orchestrator captures payment, then crashes before it writes down that the capture happened. It restarts. What goes wrong, and what one mechanism prevents it?
Reveal the answer
On restart the orchestrator has no record it already captured, so it re-sends "capture payment" and double-charges the customer. The fix is two-part: persist the state change in the same local transaction as the action (the outbox pattern), and make the capture handler idempotent on the saga ID so a repeat message is a no-op. Reaching for both unprompted is the staff-level signal here.

4Trade-offs#

The big decision is choreography versus orchestration. I have argued both sides in real design reviews, so here is the honest version.

Dimension Choreography Orchestration
Coupling Loose, services only know events Services coupled to orchestrator
Where the flow lives Spread across every service One readable state machine
Easy to add a step Hard, touch multiple services Easy, edit one place
Debugging a stuck saga Painful, trace events everywhere You query one state row
Risk of cyclic event storms Real and nasty Low
Single point of failure None The orchestrator (must be HA)
Good fit 2 to 4 simple steps 5+ steps, complex branching
Default for 5+ steps
Orchestration
Flow
One state machine you read top to bottom.
Debugging
Query one row: "where is order 8842 stuck."
Adding a step
Edit one place.
Cost
The orchestrator is a dependency you run highly available.
Keep for 2 to 4 steps
Choreography
Flow
Spread across every service's event wiring.
Debugging
Trace events across five services' logs.
Adding a step
Touch multiple services.
Cost
No single point of failure, but cyclic event storms are real.

My rule of thumb.

  • More than four steps or any real branching, I reach for orchestration.
  • Being able to look at one state machine and answer "where is order 8842 stuck and why" is worth the central dependency.
  • I have watched engineers lose an afternoon tracing a choreographed flow across five services' logs to find one dropped event.
  • The five-step order saga above is exactly why I orchestrated it: four of those steps have a failure branch that fans out to a different compensation.

The thing you give up with sagas in general, either style, is isolation.

  • In ACID, the "I" means no other transaction sees your half-done work. Sagas have no isolation.
  • Between steps, other operations can read and act on intermediate state.
  • Classic problem: a balance is debited in step 2, a second saga reads that balance in step 3 and decides on it, then step 2's saga rolls back. The second saga acted on money that no longer exists.

You handle this with application-level countermeasures.

  • Semantic locks: mark a record "pending" so others know not to trust it yet.
  • Commutative updates: design operations so order does not matter, like increment instead of set.
  • Reread values before committing.
  • These are not free, and getting them right is 80% of the hard work in a real saga.

5Where it is used#

Anywhere a single business action spans services that each own their data.

  • Order fulfillment, the textbook case. Place order, charge, reserve inventory, assign a courier, notify. Five services, five databases, one user action. E-commerce platforms run exactly this, and it is the example above.
  • Travel and ticketing. Flight plus hotel plus car, each from a different provider with its own cancellation rules. Compensations here are genuinely hard because a cancel might cost money or be time-windowed.
  • Banking and money movement. Transfers between institutions, loan disbursement, multi-step KYC onboarding. Compensation is often not "undo" but "post a correcting entry," which is cleaner because ledgers already think in reversible entries.
  • Account signup across systems. Create the user, set up billing, send to the CRM, grant the default workspace. If billing setup fails, tear down the half-created account.

When I reach for it.

  • I have a cross-service operation, I cannot use a shared database, and the business can tolerate brief windows of inconsistency.
  • If the business truly cannot tolerate any visible intermediate state, a saga is the wrong tool and you need to rethink the service boundaries. That is a legitimate answer in an interview.

When I actively avoid it.

  • Anything that fits in one local transaction. Do not build a distributed saga for an operation you could do with one COMMIT.
  • I have seen teams add an orchestrator for a two-step flow inside a single bounded context. That is complexity with no payoff.

A saga is a business decision wearing a technical costume: it only makes sense when the business can live with visible partial state.

6Common pitfalls and gotchas#

The sharp edges, each one a thing interviewers listen for.

  • Compensations that cannot fully undo. You sent the confirmation email in step 5; you cannot un-send it. The move is to send a correction ("your order was cancelled"), not pretend it never happened. Design compensations as "make it right," not "make it never happened."
  • Non-idempotent steps. Buses deliver at least once, so your "capture payment" handler will get the same message twice someday. If it is not idempotent, you double-charge that $79. Every step and compensation needs an idempotency key, usually the saga ID plus step name, checked before doing work.
  • Compensation itself fails. The refund times out. You cannot just give up, you captured $79. Retry with backoff, and if it still fails after the budget runs out, the saga parks in a "needs human" state with an alert. Build the dead-letter path from day one.
  • Losing saga state on crash. The coordinator state must be persisted transactionally with whatever action it just took. The outbox pattern (write the state change and the outgoing event in the same local transaction, then a relay publishes the event) is the standard fix.
  • The lost update from missing isolation. Two sagas touch the same record, one rolls back, the other already acted on the stale value. This is the sharpest edge and the one juniors never see coming.
  • Cyclic events in choreography. A's event triggers B, B's triggers A under some condition, and you have an infinite loop in production. Another reason I lean orchestration for anything non-trivial.
  • No end-to-end visibility. Without a correlation ID threaded through every step and log line, debugging a failed saga across five services is misery. Thread one saga ID everywhere.
Red flag that fails you here

Answering "what happens if step 3 fails" with "we roll back." There is nothing to roll back to, because the earlier steps already committed. The second you say "roll back" I know you have never run a saga. The right word is compensate, and you run them in reverse.

From the interviewer's chair

I am listening for one specific sentence before I ask for it: "a saga gives me eventual consistency, so I need the business to tolerate a visible intermediate state." Say that unprompted and you have told me you see the pattern as a business call, not a technical reflex. Skip it and I will drag it out of you, and the score drops while I do.

7How it shows up in interviews#

The prompt is usually a transactional flow across services: "Design checkout," "Design a ride-booking flow," "Design fund transfer between banks." The interviewer wants to see you reach for a saga without being told, then reason about the failure cases.

What they are really scoring

Not whether you can define a saga. Whether you can name the mechanism behind every trade: why 2PC's locks kill availability, why compensation runs backward, and who sees the inconsistency window and for how long. Mechanisms and numbers, not one-liners.

What they probe:

"Why not two-phase commit?" Weak answer: "2PC is slow." True but shallow, and it tells the interviewer you memorized a one-liner. Strong answer: "2PC holds locks across every participating service for the full duration of the transaction, and it blocks if the coordinator dies mid-flight, so a single slow or crashed node freezes everyone. That kills availability and throughput at scale. A saga trades strict consistency for availability and drops the distributed locks entirely, each step commits locally and we clean up with compensations. That trade is the whole reason sagas won in microservices." Notice the strong version names the mechanism (cross-service locks, coordinator blocking) and the trade, not just the symptom.

"Choreography or orchestration, and why?" Weak answer: "I'd use orchestration." Names one with no reasoning, so I have to drag the justification out of you. Strong answer: "This checkout flow is five steps with a failure branch at nearly every step, so I'd orchestrate it. One state machine lets me answer 'where is order 8842 stuck' from a single row, and adding a step is a one-place edit. The cost is the orchestrator becomes a dependency I have to run highly available, so I'd put it behind at least two replicas with its state in a durable store." Picks one, justifies with step count and branching and debuggability, and admits the tradeoff.

"What happens if step 3 fails?" Weak answer: "We roll back." Red flag, because there is nothing to roll back to, the earlier steps already committed. Strong answer: "There's no rollback, the capture at step 2 already happened. The orchestrator runs compensations in reverse: step 3 reserved nothing so it's a no-op, step 2 refunds the $79, step 1 marks the order cancelled. Each compensation is idempotent and keyed by saga ID, and if the refund fails I retry with backoff and park the saga for a human if it exhausts the budget."

"How do you handle the message being delivered twice?" Idempotency keys. If you cannot answer this instantly, it signals you have not run this in production.

"Where's the inconsistency window and who sees it?" This separates the people who have only read about sagas from the people who have operated one. Weak answer: "There's eventual consistency somewhere in there." Strong answer: "Between payment capture at step 2 and the refund completing, the customer has been debited $79 for an order they won't receive. Normally that's about 250 ms. If the refund call times out and we retry, it can reach 30 to 60 seconds, and a cron sweeps stuck sagas every minute to catch stragglers. The customer could see the pending charge on their statement in that tail."

Keep it concrete. Numbers help: "payment capture is 200 ms, if inventory fails we fire the refund async and the window is usually under 2 seconds, with a cron sweeping stuck sagas every minute." That is an engineer who has lived this, not read a blog post.

Follow-ups the interviewer will ask (and your answers)
"Why not just use two-phase commit?"
2PC holds locks across every participating service for the whole transaction and blocks if the coordinator dies mid-flight, so one slow or crashed node freezes everyone. That kills availability and throughput at scale. A saga drops the distributed locks, commits each step locally, and cleans up with compensations, trading strict consistency for availability. That trade is why sagas won in microservices.
"The same message gets delivered twice. Do you double-charge?"
No. Every step and compensation carries an idempotency key, usually the saga ID plus step name, checked before doing work. Buses deliver at least once, so the capture handler will see a repeat someday and must treat it as a no-op. If I could not answer this instantly it would signal I had not run one in production.
"The refund compensation itself fails. Now what?"
I cannot give up, I already captured $79. Retry with backoff, and if it exhausts the budget the saga parks in a needs-human state with an alert and lands in a dead-letter path. Build that path from day one. A saga with no story for "compensation failed" is not production ready.
"The orchestrator crashes mid-saga. How do you not double-charge?"
The coordinator persists its state transactionally with whatever action it just took, using the outbox pattern: write the state change and the outgoing event in the same local transaction, then a relay publishes the event. On restart it knows exactly which step it reached, and idempotency keys make any replayed message a no-op.
"Where is the inconsistency window and who sees it?"
Between payment capture at step 2 and the refund completing, the customer is debited $79 for an order they won't receive. Normally about 250 ms; if the refund times out and retries, 30 to 60 seconds, with a cron sweeping stuck sagas every minute. The customer could see the pending charge on their statement in that tail.

What you can now explain in the room

  • Why a saga gives eventual consistency, and why "roll back" is the wrong word for a failed step.
  • When to choose orchestration over choreography, justified by step count, branching, and debuggability.
  • How compensation runs in reverse and why later steps can depend on earlier ones.
  • Why 2PC's cross-service locks and coordinator blocking make it the wrong fit at scale.
  • Why every step needs an idempotency key, and what the outbox pattern buys you on crash.
  • Where the inconsistency window is, who sees it, and the 250 ms versus 30 to 60 second figures.

Next, try the System Design Question walkthrough on designing a checkout and order-fulfillment service, where the saga pattern earns its keep step by step.

Get a prep plan built around your Saga Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Saga Pattern prep plan is on the way.