Saga Pattern: Managing Distributed Transactions

SOFTWARE ARCHITECTURE By TryzTech Team
Saga PatternDistributed SystemsMicroservicesTransactionsArchitecture

Table of Contents

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.

StepPossible failure
Create orderdatabase timeout
Reserve inventoryitem out of stock
Charge paymentpayment provider error
Create shipmentshipping 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.

StyleHow it worksBest for
Choreographyservices react to events from each othersmaller workflows with simple dependencies
Orchestrationone coordinator tells services what to docomplex 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:

StepCommandSuccess eventFailure compensation
1Create orderOrderCreatedmark order failed
2Reserve inventoryInventoryReservedrelease inventory
3Charge paymentPaymentCapturedrefund payment
4Create shipmentShipmentCreatedcancel shipment

The saga coordinator tracks progress:

order_idstatelast_step
O1001payment_capturedcharge payment
O1002failedreserve inventory
O1003completedcreate 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:

ToolGood fit
Temporaldurable code-based workflows, useful for long-running orchestration
Camunda / ZeebeBPMN-based business processes, useful when workflows need to be visible to business and engineering teams
AWS Step Functionsserverless orchestration in the AWS ecosystem
Azure Durable Functionsdurable workflows in the Azure ecosystem
MassTransitsaga state machines and routing slips in the .NET/message bus ecosystem
Netflix Conductorworkflow 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.

Keep reading within the same topic.

Don't Miss Out

Get the latest tech articles, tips, and insights delivered to your inbox.