blog/system-design/saga-pattern-distributed-transactions
System Design & Architecture

The Saga Pattern: Distributed Transactions Without Distributed Transactions

A single business action spans three services and there's no ACID transaction to save you. The saga pattern trades atomicity for a chain of local transactions and compensations. Here's how it works, when to reach for it, and the guarantees you quietly give up.

·9 min read

The Problem Nobody Warns You About

You split the monolith. Orders, payments, and inventory each got their own service and their own database. Clean boundaries, independent deploys, happy teams.

Then a customer places an order. You need to: reserve inventory, charge their card, and create the order record. In the monolith, that was one database transaction — all three succeed or all three roll back. Now those three writes live in three databases across the network.

What happens when inventory reserves successfully, the card charge goes through, but the order-creation write fails? You've taken the customer's money and locked stock for an order that doesn't exist.

There's no BEGIN TRANSACTION that spans three services. The saga pattern is how you get something useful in its place.


Why You Can't Just Use a Distributed Transaction

The textbook answer is two-phase commit (2PC): a coordinator asks every service to "prepare," and once all say yes, it tells them to "commit." It gives you atomicity across services.

In practice, almost nobody runs 2PC across microservices, and for good reasons:

⚠️ What 2PC actually costs you

During the prepare phase, every participant holds locks and waits for the coordinator's decision. If the coordinator crashes after "prepare" but before "commit," participants are stuck holding locks indefinitely — this is the blocking problem. Throughput collapses under contention, and your availability is now tied to the slowest, least reliable participant.

2PC optimizes for consistency at the cost of availability and latency. Most business systems want the opposite: stay available, keep latency low, and accept that consistency will be eventual. That trade-off is exactly what a saga makes.


What a Saga Actually Is

A saga is a sequence of local transactions. Each step commits to its own database and publishes an event or sends a command that triggers the next step. There is no global lock and no global commit.

The catch: since each local transaction commits immediately, you can't "roll back" once a later step fails. Instead, every step has a matching compensating transaction — an action that semantically undoes it.

📌 Compensating Transaction

A compensating transaction is not a database rollback. It's a new transaction that reverses the business effect of a completed step. You didn't charge the card and undo it — you charged the card, then issued a refund. Both are real, recorded events. The account balance ends up correct, but the history shows both.

Here's the happy path for placing an order:

Order Saga — Happy Path
Order
create pending
OrderCreated
Payment
charge card
Inventory
reserve stock
Shipping
schedule

Each arrow is an event. Each box is a local transaction that already committed by the time the next one starts. Nothing is holding a lock waiting on the others.


What Happens When a Step Fails

Say inventory can't reserve the stock — the item sold out between the customer clicking "buy" and the reservation step running. The saga now runs backward, firing the compensating transaction for every step that already succeeded.

1

Inventory fails

The reserve-stock step throws. There's nothing to compensate here yet — the failing step didn't commit anything.

2

Compensate payment

The saga triggers a refund. This is a brand-new transaction in the payment service. The original charge still happened and is still in the ledger — the refund sits next to it.

3

Compensate order

The order is marked cancelled (not deleted — you want the audit trail). The customer sees "order failed, payment refunded."

The system reaches a consistent state, but notice what you gave up: for a brief window, the customer was charged for an order that ultimately failed. That window is the price of not using 2PC.

🔴 Sagas have no isolation

This is the guarantee people forget. While a saga is mid-flight, other transactions can see its partial results. Another request might read the "pending" order, or see stock that's reserved-then-released. ACID gives you the I in isolation; sagas do not. You have to design for dirty reads — usually with status flags (pending, confirmed, cancelled) so nothing treats an in-progress saga as final.


Two Ways to Coordinate: Choreography vs Orchestration

Once you accept the saga model, the real design decision is who drives the sequence. There are two answers.

Choreography — no central brain. Each service listens for events and reacts. Payment listens for OrderCreated, does its work, and emits PaymentCharged. Inventory listens for that, and so on. The workflow is an emergent property of who-subscribes-to-what.

Orchestration — a central coordinator (the orchestrator) explicitly tells each service what to do and waits for the reply. The workflow lives in one place: the orchestrator's state machine.

Orchestration — One Coordinator Drives Every Step
createchargereserveOrchestratorsaga stateOrderPaymentInventory

The trade-off is about where complexity lives, not whether it exists.

DimensionChoreographyOrchestration
Workflow logicScattered across servicesCentralized in one orchestrator
CouplingLoose — services only know eventsServices coupled to orchestrator
VisibilityHard — no single place shows progressEasy — orchestrator holds saga state
Adding a stepWire up new event subscriptionsEdit the orchestrator's flow
Cyclic riskEasy to create event loops by accidentExplicit flow prevents cycles
Single point of failureNone (fully distributed)Orchestrator (needs HA)

The rough rule: choreography for short, stable sagas (2–4 steps that rarely change), orchestration once the workflow gets long or branchy. When a saga has five-plus steps with conditional paths, tracing "why is this order stuck" across a web of event subscriptions becomes miserable. An orchestrator gives you one place to look.

Choreography or Orchestration?

How many steps does your saga have?


The Two Rules That Keep Sagas From Corrupting Data

Sagas run over unreliable networks. Messages get lost, delivered twice, or arrive out of order. Two properties are non-negotiable.

1. Every step must be idempotent

A PaymentCharged event can be delivered twice. If your payment handler charges the card each time it sees the event, retries turn into double charges. Every step needs to detect "I've already processed this" and no-op the second time — usually by tracking a unique saga/message ID.

2. Compensations must always eventually succeed

A compensating transaction cannot be allowed to just fail and give up — that leaves the system permanently inconsistent. Compensations are retried until they succeed, which means they too must be idempotent, and they must be designed to always be possible.

⚠️ Some things can't be compensated

You can refund a charge. You cannot un-send an email, un-ship a package, or un-launch a missile. For irreversible steps, order your saga so they run last, after every step that might fail has already committed. If the irreversible action is early and a later step fails, you're stuck. Sequencing is a design decision, not an afterthought.


When a Saga Is the Right Tool

Sagas are not free. They add compensating logic, idempotency handling, and a whole new class of "partially completed" states to reason about. Reach for them when the alternatives are worse:

  • The operation genuinely spans services that own separate databases, and you've accepted eventual consistency. This is the core case.
  • Long-running business processes — anything that waits on a human approval, an external provider, or a timer measured in minutes to days. You can't hold a database transaction open that long; a saga persists the state instead.
  • You need an audit trail of what happened, including the failures and reversals. Sagas record every step and compensation as real events.

And when to avoid them:

  • The data lives in one database. Just use a local ACID transaction. Don't build a saga to coordinate two tables you could wrap in BEGIN/COMMIT.
  • You truly need isolation — no other operation may ever observe an intermediate state. Sagas can't give you that. Rethink the boundaries or keep it in one service.

A useful reframing

A saga is a state machine for a business process, where every transition has an "undo." If you can't clearly describe your process as states, transitions, and compensations, you don't understand the workflow well enough to distribute it yet.


Key Takeaways

  • A saga replaces one distributed ACID transaction with a sequence of local transactions, each with a compensating transaction to undo its business effect.
  • You trade atomicity and isolation for availability and eventual consistency. Partial states are visible mid-flight — design for them with explicit status flags.
  • Choreography (event-driven, decentralized) suits short stable flows; orchestration (central coordinator) suits long, branching, or evolving flows where visibility matters.
  • Steps and compensations must be idempotent, and compensations must eventually succeed. Put irreversible actions last.
  • If your data fits in one database, you don't need a saga — you need a transaction.

References