Skip to content

Saga

Each service now owns its own database. Placing an order, though, is one business operation that spans three of them: the Order service records the order, the Customer service reserves the buyer’s credit, and the Inventory service holds the stock. In the monolith this was a single transaction — all three changes committed together or none did. With three private databases, that single transaction is impossible.

You might reach for a distributed transaction. The classic mechanism, two-phase commit (2PC), asks every participant to prepare, then tells them all to commit. It does give you atomicity, but at a steep price: every participant holds locks for the whole duration, one slow or crashed participant stalls everyone, and many modern data stores and message brokers do not support it at all. 2PC trades availability and scalability for strong consistency — the opposite of what you decomposed your system to get.

So the forces are: you need a multi-service operation to either fully complete or fully unwind, but you cannot hold a global lock, you cannot assume every service shares a transaction manager, and you must stay available even when one service is slow.

A saga is a sequence of local transactions. Each step runs entirely inside one service, commits to that service’s own database, and then triggers the next step. Because each step commits independently, there is no global lock and no shared transaction manager.

The catch is rollback. Once a local transaction commits, you cannot simply roll it back later. Instead, each step has a compensating transaction — a semantically inverse local transaction that undoes its effect. If a step fails partway through the saga, the saga runs the compensations for every step that already succeeded, in reverse order, returning the system to a consistent state.

There are two ways to coordinate the steps:

  • Choreography — there is no central coordinator. Each service listens for events from the previous step and reacts by performing its local transaction and publishing its own event. The workflow is the sum of the subscriptions.
  • Orchestration — a single coordinator (the orchestrator) tells each service what to do and waits for the reply, driving the saga step by step and deciding when to compensate.

Choreography: services react to each other’s events

Section titled “Choreography: services react to each other’s events”
sequenceDiagram
  participant O as Order Service
  participant C as Customer Service
  participant I as Inventory Service
  O->>O: create order (PENDING)
  O-->>C: OrderCreated
  C->>C: reserve credit
  C-->>I: CreditReserved
  I->>I: reserve stock
  alt stock available
    I-->>O: StockReserved
    O->>O: approve order (CONFIRMED)
  else out of stock
    I-->>C: StockReservationFailed
    C->>C: release credit (compensate)
    C-->>O: CreditReleased
    O->>O: reject order (CANCELLED)
  end
Choreography saga — each service reacts to events; compensation cascades backward on failure

Orchestration: a coordinator drives each step

Section titled “Orchestration: a coordinator drives each step”
sequenceDiagram
  participant SO as Order Saga Orchestrator
  participant O as Order Service
  participant C as Customer Service
  participant I as Inventory Service
  SO->>O: createOrder()
  O-->>SO: ok (order PENDING)
  SO->>C: reserveCredit()
  C-->>SO: ok
  SO->>I: reserveStock()
  alt stock available
    I-->>SO: ok
    SO->>O: approveOrder()
    O-->>SO: order CONFIRMED
  else out of stock
    I-->>SO: failed
    SO->>C: releaseCredit() (compensate)
    C-->>SO: ok
    SO->>O: rejectOrder() (compensate)
    O-->>SO: order CANCELLED
  end
Orchestration saga — a central coordinator issues commands and triggers compensations on failure

Here is an orchestration saga for the order flow. The orchestrator runs the forward steps in order; if a step throws, it runs the compensations for the steps that already succeeded, in reverse. Each example is self-contained and idiomatic to its language.

type Ctx = { orderId: string; customerId: string; amount: number; sku: string };
interface Step {
name: string;
action: (ctx: Ctx) => Promise<void>;
compensate: (ctx: Ctx) => Promise<void>;
}
async function runSaga(ctx: Ctx, steps: Step[]): Promise<void> {
const done: Step[] = [];
try {
for (const step of steps) {
await step.action(ctx);
done.push(step);
}
} catch (err) {
// Undo completed steps in reverse order.
for (const step of done.reverse()) {
await step.compensate(ctx).catch((e) =>
console.error(`compensation failed for ${step.name}`, e),
);
}
throw err;
}
}
const orderSaga: Step[] = [
{
name: 'createOrder',
action: (c) => orderSvc.create(c.orderId, c.customerId),
compensate: (c) => orderSvc.reject(c.orderId),
},
{
name: 'reserveCredit',
action: (c) => customerSvc.reserveCredit(c.customerId, c.amount),
compensate: (c) => customerSvc.releaseCredit(c.customerId, c.amount),
},
{
name: 'reserveStock',
action: (c) => inventorySvc.reserve(c.sku),
compensate: (c) => inventorySvc.release(c.sku),
},
];
await runSaga(ctx, orderSaga);

What you gain:

  • Consistency without 2PC. Multi-service operations either complete fully or unwind through compensation, with no global locks and no shared transaction manager — services stay available and loosely coupled.
  • Works with anything. Because each step is a plain local transaction plus a message, sagas work across any mix of databases and brokers.

What it costs you:

  • Eventual consistency, not isolation. Between steps the system is in a partial state. Another transaction could observe an order whose credit is reserved but whose stock is not yet confirmed. You must design for these intermediate states (for example, with a semantic lock such as a PENDING status) rather than rely on database isolation.
  • You must design compensations. Every step that has an observable effect needs a sensible inverse, and compensations must be idempotent and able to handle being retried.
  • Coordination complexity. Choreography spreads the workflow across many subscriptions and can become hard to follow as steps grow; orchestration centralizes the logic but adds a component that must itself be reliable and recoverable.

A practical rule: prefer choreography for short, simple flows with few participants, and orchestration once the flow has branches, several steps, or needs to be reasoned about as a whole.

  • Database per Service — the boundary that makes sagas necessary.
  • Event Sourcing — a reliable way to publish the events a saga depends on.
  • CQRS — often used to query the intermediate state a saga produces.
What is a saga, at its core?
How does a saga undo work that has already committed in an earlier step?
What distinguishes orchestration from choreography?
Which is a real drawback of sagas compared with a single ACID transaction?