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
Local LLMs in Your IDE: Connecting Ollama to Coding Agents and Autocomplete
Wire local models into VS Code, JetBrains, Cline, Continue, and Aider via the OpenAI-compatible API. Covers model routing, context budgets, tool calling with small models, and when a local model is the right choice for the job.
15 min readBuilding a Self-Hosted AI Stack: Ollama, Open WebUI, and Local RAG
Stand up a fully self-hosted AI stack on a single machine: Ollama for inference, Open WebUI as the chat interface, local embeddings for RAG, and a reverse proxy for secure access. No cloud dependency, no data leaving your network.
17 min readTop 5 Open-Source Coding Models to Run on Your Mac (2026)
The best local coding models for Apple Silicon in 2026, ranked by quality per gigabyte of unified memory. Covers qwen3-coder, devstral, gpt-oss, and more with real pull tags, sizes, and context windows.
14 min readRunning LLMs Locally: GGUF, Quantization, and Memory Planning
Learn the GGUF format, the quantization ladder from Q2 to FP16, and the exact memory math for running models on Apple Silicon and NVIDIA GPUs. Includes Ollama and llama.cpp tuning for KV cache and context.
15 min readOllama vs vLLM vs llama.cpp: Choosing the Right Local LLM Runtime
Compare the three dominant local LLM runtimes on architecture, throughput, hardware, and deployment context. Includes benchmark data, a decision framework, and a migration path from Ollama to vLLM.
16 min readContinue Reading
Local LLMs in Your IDE: Connecting Ollama to Coding Agents and Autocomplete
Wire local models into VS Code, JetBrains, Cline, Continue, and Aider via the OpenAI-compatible API. Covers model routing, context budgets, tool calling with small models, and when a local model is the right choice for the job.
15 min readBuilding a Self-Hosted AI Stack: Ollama, Open WebUI, and Local RAG
Stand up a fully self-hosted AI stack on a single machine: Ollama for inference, Open WebUI as the chat interface, local embeddings for RAG, and a reverse proxy for secure access. No cloud dependency, no data leaving your network.
17 min readTop 5 Open-Source Coding Models to Run on Your Mac (2026)
The best local coding models for Apple Silicon in 2026, ranked by quality per gigabyte of unified memory. Covers qwen3-coder, devstral, gpt-oss, and more with real pull tags, sizes, and context windows.
14 min readShip Faster. Ship Safer.
Join thousands of engineering teams using MatterAI to autonomously build, review, and deploy code with enterprise-grade precision.
