Saga Pattern: Managing Distributed Transactions
Table of Contents
- Introduction
- Why Distributed Transactions Are Hard
- What the Saga Pattern Does
- Choreography vs Orchestration
- Example: Order Checkout Saga
- Saga Orchestration Diagram
- Saga Orchestrator Code Example
- Compensating Actions
- Libraries and Tools You Can Use
- Operational Concerns
- Common Mistakes
- Checklist
- FAQ
- Conclusion
Introduction
In a monolith, a checkout flow can often run inside one database transaction. Create the order, reserve inventory, charge payment, and commit everything together.
In a distributed system, those steps may live in different services. The order service, inventory service, payment service, and shipping service each own their data. A single database transaction no longer covers the whole workflow.
The saga pattern is a way to coordinate long-running business workflows across services without pretending everything can be atomic.
Why Distributed Transactions Are Hard
Distributed transactions are hard because each service can fail independently.
| Step | Possible failure |
|---|---|
| Create order | database timeout |
| Reserve inventory | item out of stock |
| Charge payment | payment provider error |
| Create shipment | shipping service unavailable |
If payment succeeds but shipment fails, the system needs a business decision. Should it retry shipment? Cancel the order? Refund the payment? Ask for manual review?
Saga makes those decisions explicit.
What the Saga Pattern Does
A saga breaks one large business transaction into a sequence of smaller local transactions.
Each step commits locally. If a later step fails, the saga runs compensating actions to undo or offset earlier steps.
Create order -> Reserve stock -> Charge payment -> Create shipment
If charging payment fails:
Release stock -> Mark order as failed
Compensation is not always a perfect rollback. It is a business action that makes the system consistent again.
Choreography vs Orchestration
There are two common ways to implement sagas.
| Style | How it works | Best for |
|---|---|---|
| Choreography | services react to events from each other | smaller workflows with simple dependencies |
| Orchestration | one coordinator tells services what to do | complex workflows that need visibility and control |
Choreography can feel lightweight, but it may become hard to trace. Orchestration is easier to observe, but it introduces a central workflow component.
Example: Order Checkout Saga
An orchestrated checkout saga might look like this:
| Step | Command | Success event | Failure compensation |
|---|---|---|---|
| 1 | Create order | OrderCreated | mark order failed |
| 2 | Reserve inventory | InventoryReserved | release inventory |
| 3 | Charge payment | PaymentCaptured | refund payment |
| 4 | Create shipment | ShipmentCreated | cancel shipment |
The saga coordinator tracks progress:
| order_id | state | last_step |
|---|---|---|
| O1001 | payment_captured | charge payment |
| O1002 | failed | reserve inventory |
| O1003 | completed | create shipment |
This state is important for retries, debugging, and support.
Saga Orchestration Diagram
Here is an example checkout saga using orchestration. CheckoutSaga acts as the coordinator that decides the next step, stores state, and runs compensation when a step fails.
sequenceDiagram
autonumber
participant User
participant Saga as CheckoutSaga
participant Order as Order Service
participant Inventory as Inventory Service
participant Payment as Payment Service
participant Shipping as Shipping Service
User->>Saga: Place order
Saga->>Order: Create order
Order-->>Saga: OrderCreated
Saga->>Inventory: Reserve inventory
Inventory-->>Saga: InventoryReserved
Saga->>Payment: Capture payment
alt payment succeeds
Payment-->>Saga: PaymentCaptured
Saga->>Shipping: Create shipment
Shipping-->>Saga: ShipmentCreated
Saga-->>User: Checkout completed
else payment fails
Payment-->>Saga: PaymentFailed
Saga->>Inventory: Release inventory
Saga->>Order: Mark order failed
Saga-->>User: Checkout failed
end
With choreography, the diagram would look different. There is no central coordinator. Each service publishes an event, then other services react to that event. This can be simpler for small workflows, but it is usually harder to trace once the flow starts branching.
Saga Orchestrator Code Example
The following example is intentionally small. It is not meant to be a production-ready framework. It shows how an orchestrator can run steps in order and call compensation in reverse order when a failure happens.
type SagaContext = {
orderId: string;
userId: string;
items: Array<{ sku: string; quantity: number }>;
paymentId?: string;
shipmentId?: string;
};
type SagaStep = {
name: string;
execute: (context: SagaContext) => Promise<void>;
compensate: (context: SagaContext) => Promise<void>;
};
async function runSaga(context: SagaContext, steps: SagaStep[]) {
const completedSteps: SagaStep[] = [];
try {
for (const step of steps) {
await step.execute(context);
completedSteps.push(step);
await saveSagaState(context.orderId, step.name, "completed");
}
await saveSagaState(context.orderId, "checkout", "completed");
} catch (error) {
await saveSagaState(context.orderId, "checkout", "compensating");
for (const step of completedSteps.reverse()) {
await step.compensate(context);
await saveSagaState(context.orderId, step.name, "compensated");
}
await saveSagaState(context.orderId, "checkout", "failed");
throw error;
}
}
Usage:
const checkoutSteps: SagaStep[] = [
{
name: "create_order",
execute: async (context) => {
await orderService.createOrder(context.orderId, context.userId, context.items);
},
compensate: async (context) => {
await orderService.markOrderFailed(context.orderId);
},
},
{
name: "reserve_inventory",
execute: async (context) => {
await inventoryService.reserve(context.orderId, context.items);
},
compensate: async (context) => {
await inventoryService.release(context.orderId);
},
},
{
name: "capture_payment",
execute: async (context) => {
context.paymentId = await paymentService.capture(context.orderId);
},
compensate: async (context) => {
if (context.paymentId) {
await paymentService.refund(context.paymentId);
}
},
},
{
name: "create_shipment",
execute: async (context) => {
context.shipmentId = await shippingService.createShipment(context.orderId);
},
compensate: async (context) => {
if (context.shipmentId) {
await shippingService.cancelShipment(context.shipmentId);
}
},
},
];
await runSaga(
{
orderId: "O1004",
userId: "U2001",
items: [{ sku: "SKU-1", quantity: 2 }],
},
checkoutSteps,
);
This tiny example does not handle several production concerns yet: retries with backoff, per-step timeouts, idempotency keys, concurrency control, dead-letter queues, and recovery if the process dies halfway through. In production, those parts are usually handled by a workflow engine or messaging framework.
Compensating Actions
Compensating actions are the heart of saga design.
Examples:
- release reserved inventory
- refund a payment
- cancel a shipment
- mark an order as failed
- create a support ticket for manual handling
Good compensation should be idempotent. If the system retries the compensation, it should not create a second refund or release the same inventory twice.
Libraries and Tools You Can Use
You can implement saga yourself, but important workflows are usually safer with a library or workflow engine that already handles state, retries, timeouts, observability, and recovery.
Common options:
| Tool | Good fit |
|---|---|
| Temporal | durable code-based workflows, useful for long-running orchestration |
| Camunda / Zeebe | BPMN-based business processes, useful when workflows need to be visible to business and engineering teams |
| AWS Step Functions | serverless orchestration in the AWS ecosystem |
| Azure Durable Functions | durable workflows in the Azure ecosystem |
| MassTransit | saga state machines and routing slips in the .NET/message bus ecosystem |
| Netflix Conductor | workflow orchestration for microservices |
For a small system, a simple implementation with a database state table and message queue may be enough. Once the workflow becomes longer, branches more often, or needs an audit trail, a workflow engine usually becomes the better choice.
Operational Concerns
Saga is not only a design pattern. It needs operational support.
You need:
- durable saga state
- retry rules
- timeout handling
- idempotency keys
- dead-letter queues or manual review paths
- visibility into stuck workflows
- clear business ownership for compensation rules
Without observability, a saga can fail silently and leave users confused.
Common Mistakes
Treating compensation as a technical rollback
Compensation is a business decision. A refund, cancellation, or manual review may all be valid depending on the domain.
Forgetting idempotency
Messages can be delivered more than once. Every command and compensation should be safe to retry.
Hiding saga state
If support and engineering cannot see where a workflow is stuck, incidents become harder to resolve.
Overusing saga for simple workflows
Not every multi-step process needs saga complexity. Use it when the workflow crosses service boundaries and failure handling matters.
Checklist
- Define each local transaction clearly.
- Define compensation for each step.
- Make commands and compensation idempotent.
- Store saga state durably.
- Add timeouts and retry policies.
- Track correlation IDs across services.
- Expose stuck workflows for support or operations.
- Decide when manual intervention is required.
- Test failure at every step.
FAQ
Is saga the same as two-phase commit?
No. Two-phase commit tries to make distributed work atomic. Saga accepts local commits and uses compensation when later steps fail.
Should I use choreography or orchestration?
Use choreography for simple event chains. Use orchestration when the workflow has many branches, strong ordering, or needs central visibility.
Can saga guarantee strong consistency?
No. Saga usually provides eventual consistency. The system may be temporarily in an intermediate state. For reliable audit trails and state tracking, it is frequently paired with Event Sourcing and the CQRS Pattern. Additionally, keeping saga orchestrators inside the Application Layer aligns well with Hexagonal Architecture to isolate business rules from messaging infrastructure.
Conclusion
The saga pattern helps teams manage business workflows across distributed services. It does not remove failure, but it gives the system a clear way to respond when failure happens.
Design the workflow, compensation, retries, and observability together. That is what turns saga from a diagram into a production-ready pattern.
Related Articles
Keep reading within the same topic.
Circuit Breaker Pattern: Building Resilient Systems
Learn the circuit breaker pattern with closed, open, and half-open states, retries, timeouts, fallbacks, observability, and production trade-offs.
Event Sourcing: Building Auditable Systems
Understand event sourcing with practical examples, event streams, projections, audit trails, trade-offs, and when this architecture pattern is worth using.
Microservices Architecture: When to Use It and Key Patterns
Decide when microservices make sense, then learn key patterns like API gateway, service discovery, circuit breaker, and deployment strategy.
Hexagonal Architecture: Ports and Adapters Explained
Understand hexagonal architecture with ports, adapters, dependency direction, testing benefits, examples, trade-offs, and practical implementation guidance.