Saga Pattern: Distributed Transactions Without Two-Phase Commit
Saga Pattern: Distributed Transactions Without Two-Phase Commit
A distributed transaction across services cannot use a database transaction: there is no single database. Two-phase commit (2PC) exists but is slow, fragile, and locks resources across the network. The saga pattern is the practical alternative: a sequence of local transactions, each with a compensating action that undoes it if a later step fails.
The Problem
Consider an order flow spanning three services:
orders: create order
payments: charge card
inventory: reserve stock
If the payment succeeds but inventory fails, you have an inconsistent state: a paid order with no stock. A local transaction cannot span these services. The saga pattern solves this with compensation:
Step 1: create order compensation: cancel order
Step 2: charge card compensation: refund card
Step 3: reserve stock compensation: release stock
If step 3 fails, run compensations in reverse: release nothing, refund the card, cancel the order.
Choreography vs Orchestration
There are two ways to coordinate a saga.
Choreography: each service publishes events and reacts to events. No central coordinator.
orders ──order.created──► payments ──payment.charged──► inventory
▲ │
└──────────────order.cancelled◄──payment.failed─────────┘
Orchestration: a central coordinator (the saga) tells each service what to do and tracks the state.
┌──────────────┐
│ order-saga │
└──────┬───────┘
create order │ charge card
┌──────────────────┼──────────────────┐
▼ ▼ ▼
orders payments inventory
| Aspect | Choreography | Orchestration |
|---|---|---|
| Coupling | Loose (events only) | Tighter (coordinator) |
| Visibility | Hard to trace | Central state machine |
| Failure handling | Distributed, complex | Centralized, explicit |
| Best for | Simple, few services | Complex, many services |
Orchestrated Saga: The State Machine
The orchestrated saga is a state machine. Each step is a command; each response advances the state:
// saga.ts — orchestrated saga with explicit states
type SagaState =
| { status: "pending" }
| { status: "order-created"; orderId: string }
| { status: "payment-charged"; orderId: string; paymentId: string }
| { status: "completed"; orderId: string }
| { status: "compensating"; orderId: string }
| { status: "failed"; orderId: string; reason: string };
const saga = {
async run(orderId: string): Promise<SagaState> {
let state: SagaState = { status: "pending" };
try {
await ordersClient.createOrder(orderId);
state = { status: "order-created", orderId };
await paymentsClient.chargeCard(orderId);
state = { status: "payment-charged", orderId, paymentId: "pay_123" };
await inventoryClient.reserveStock(orderId);
state = { status: "completed", orderId };
return state;
} catch (err) {
await this.compensate(state, err);
return { status: "failed", orderId, reason: String(err) };
}
},
async compensate(state: SagaState, err: unknown): Promise<void> {
// Run compensations in reverse order of completed steps
if (state.status === "payment-charged") {
await paymentsClient.refundCard(state.paymentId);
}
if (state.status === "order-created") {
await ordersClient.cancelOrder(state.orderId);
}
},
};
Choreographed Saga: Events and Handlers
Choreography uses events and idempotent handlers:
// payments service — reacts to order.created, emits payment.charged
broker.subscribe("order.created", async (event) => {
try {
await chargeCard(event.orderId);
await broker.publish("payment.charged", { orderId: event.orderId });
} catch (err) {
await broker.publish("payment.failed", {
orderId: event.orderId,
reason: String(err),
});
}
});
// orders service — reacts to payment.failed, cancels the order
broker.subscribe("payment.failed", async (event) => {
await cancelOrder(event.orderId);
await broker.publish("order.cancelled", { orderId: event.orderId });
});
Choreography is simpler to write and looser in coupling, but the flow is implicit: you cannot see the whole saga in one place, and a missed event handler silently breaks the flow.
The Outbox Pattern: Reliable Event Publishing
Both styles depend on reliable event delivery. The outbox pattern guarantees that a state change and its event are published together:
// Write the state change and the outbox row in ONE local transaction
await db.transaction(async (tx) => {
await tx.order.update({ id: orderId, status: "paid" });
await tx.outbox.create({
id: crypto.randomUUID(),
type: "order.paid",
payload: { orderId },
publishedAt: null,
});
});
// A relay worker polls the outbox and publishes to the broker
async function relayOutbox() {
const rows = await db.outbox.findMany({
where: { publishedAt: null },
take: 100,
});
for (const row of rows) {
await broker.publish(row.type, row.payload);
await db.outbox.update({
where: { id: row.id },
data: { publishedAt: new Date() },
});
}
}
Without the outbox, you get the classic failure: the transaction commits, the event publish fails, and the saga stalls forever.
Idempotency: The Non-Negotiable Requirement
Sagas retry. Retries mean duplicate messages. Every handler must be idempotent:
// Idempotent handler — dedupe by event ID
broker.subscribe("payment.charged", async (event) => {
const processed = await db.processedEvents.findUnique({
where: { eventId: event.id },
});
if (processed) return; // already handled
await db.transaction(async (tx) => {
await tx.inventory.reserve({ orderId: event.orderId });
await tx.processedEvents.create({ eventId: event.id });
});
});
Compensation Design Rules
- Compensations must be reliable: they can fail too. Retry them; if they keep failing, alert a human.
- Compensations must be idempotent: refunding twice is a bug.
- Design for partial failure: a compensation may run after the original step partially completed.
- Log everything: the saga state and every compensation attempt belong in an audit trail.
When NOT to Use a Saga
- If the steps can live in one service: a local transaction is strictly better.
- If eventual consistency is unacceptable: sagas are eventually consistent by design. If you need read-your-writes across services, reconsider the architecture.
- If you can use a single database: one database with transactions beats a saga every time.
Implementation Checklist
- Choose choreography (simple) or orchestration (complex)
- Define compensating actions for every step
- Use the outbox pattern for reliable event publishing
- Make every handler idempotent
- Add retries with backoff for compensations
- Persist saga state for recovery and audit
- Alert on compensation failures
- Consider whether a local transaction would suffice
MatterAI builds frontier AI infrastructure for engineering teams — from inference-optimized models to autonomous coding agents and agentic code reviews.
Explore what we're building:
- Orbital IDE — Autonomous AI coding agent with background agents and deep codebase memory
- AI Code Reviews — Agentic pre-commit reviews across GitHub, GitLab, and Bitbucket
- Axon Models — Frontier-grade reasoning models at 70% lower inference cost
Share this Guide:
More Guides
Model Context Protocol (MCP): Building MCP Servers from Scratch
Build production-grade MCP servers with the TypeScript and Python SDKs. Covers the MCP architecture, stdio and HTTP transports, tools, resources, prompts, and the security model every AI application needs.
16 min readRAG vs Fine-Tuning: When to Use Each for Your LLM Application
Decide between retrieval-augmented generation and fine-tuning with a practical decision framework. Compare cost, latency, freshness, and accuracy, and see working implementations of both approaches.
13 min readGrafana Stack vs SigNoz: Choosing an OpenTelemetry-Native Observability Platform
Compare the Grafana LGTM stack (Loki, Tempo, Mimir) with SigNoz for OpenTelemetry observability: architecture, storage engines, operational overhead, cost at scale, and migration paths.
10 min readLLM Fine-Tuning: LoRA vs QLoRA vs Full Fine-Tuning
Compare full fine-tuning, LoRA, and QLoRA for LLMs with memory requirements, training recipes, and code. Learn which method fits your GPU budget, dataset size, and quality requirements.
14 min readPrompt Injection Defense: Securing LLM Apps Against Jailbreaks, RAG Poisoning, and Tool-Use Exploits
Defend production LLM applications against direct jailbreaks, indirect injection via RAG pipelines, and tool-use exploits with layered defenses, input filtering, and least-privilege tool design.
10 min readContinue Reading
Model Context Protocol (MCP): Building MCP Servers from Scratch
Build production-grade MCP servers with the TypeScript and Python SDKs. Covers the MCP architecture, stdio and HTTP transports, tools, resources, prompts, and the security model every AI application needs.
16 min readRAG vs Fine-Tuning: When to Use Each for Your LLM Application
Decide between retrieval-augmented generation and fine-tuning with a practical decision framework. Compare cost, latency, freshness, and accuracy, and see working implementations of both approaches.
13 min readGrafana Stack vs SigNoz: Choosing an OpenTelemetry-Native Observability Platform
Compare the Grafana LGTM stack (Loki, Tempo, Mimir) with SigNoz for OpenTelemetry observability: architecture, storage engines, operational overhead, cost at scale, and migration paths.
10 min readShip Faster. Ship Safer.
Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.
