Microservices Saga Pattern

In a traditional database, a transaction is atomic — either everything succeeds or nothing changes. In a microservices system, a business operation often spans multiple services, each with its own database. You cannot use a single database transaction across service boundaries. The Saga Pattern solves this problem.

The Distributed Transaction Problem

Consider placing an order on an e-commerce site. Three things must succeed together:

  • Order Service creates the order.
  • Inventory Service reserves the items.
  • Payment Service charges the card.

If the payment fails after the inventory is reserved and the order is created, the system is in a broken state — reserved stock that will never ship, and an order with no payment.

BROKEN DISTRIBUTED TRANSACTION
================================
Step 1: Order Service       creates order ORD-9900     SUCCESS
Step 2: Inventory Service   reserves 2x SKU-005        SUCCESS
Step 3: Payment Service     charges card               FAIL (card declined)

Result:
  - Order ORD-9900 exists in Order DB
  - 2 units of SKU-005 locked in Inventory DB
  - No payment taken
  - Customer sees an error
  - Stock is stuck reserved indefinitely

What Is a Saga

A Saga breaks a multi-service transaction into a sequence of local transactions — one per service. Each local transaction updates that service's own database and either triggers the next step or, if something fails, triggers a compensating transaction to undo the previous steps.

Think of booking a holiday package. You book a flight, then a hotel, then a car rental. If the car rental fails, you cancel the hotel and the flight in reverse order. Each cancellation is a compensating action that undoes a completed booking step.

Two Saga Styles

Style 1: Choreography-Based Saga

Services react to events from each other. No central coordinator exists. Each service listens for events and decides what to do next.

CHOREOGRAPHY SAGA: Place Order
================================
[Order Service]
  Creates order (status: PENDING)
  Publishes: OrderCreated

[Inventory Service] hears OrderCreated
  Reserves items
  Publishes: InventoryReserved

[Payment Service] hears InventoryReserved
  Charges card
  Publishes: PaymentSucceeded  (if OK)
         OR: PaymentFailed     (if card declined)

[Inventory Service] hears PaymentFailed
  Releases reserved items
  Publishes: InventoryReleased

[Order Service] hears PaymentFailed
  Marks order as CANCELLED

Final state: no stock locked, no charge, order cancelled. Clean.

Choreography keeps services loosely coupled — no service knows about the others directly. The downside is that the overall flow is harder to visualize because no single place describes the full sequence.

Style 2: Orchestration-Based Saga

A central Saga Orchestrator service directs each step. It tells services what to do and handles failures by issuing compensating commands.

ORCHESTRATION SAGA: Place Order
=================================
[Saga Orchestrator] starts:

Step 1: Tells Order Service    --> "Create order ORD-9900"
        Order Service replies  --> "Done"

Step 2: Tells Inventory Service --> "Reserve items for ORD-9900"
        Inventory replies       --> "Done"

Step 3: Tells Payment Service  --> "Charge card for ORD-9900"
        Payment replies        --> "FAILED: card declined"

Orchestrator runs compensation:

Step 3b: Tells Inventory Service --> "Release reservation for ORD-9900"
Step 2b: Tells Order Service     --> "Cancel order ORD-9900"

Final state: system clean, no dangling data.

Orchestration makes the flow explicit and easy to understand. The downside is that the orchestrator becomes a more complex service to build and maintain.

Choreography vs Orchestration Comparison

+----------------------+------------------------+------------------------+
| Aspect               | Choreography           | Orchestration          |
+----------------------+------------------------+------------------------+
| Who controls flow?   | Each service reacts    | Central orchestrator   |
|                      | to events              | directs each step      |
+----------------------+------------------------+------------------------+
| Coupling             | Loose — services only  | Moderate — orchestrator|
|                      | know the event bus     | knows all participants |
+----------------------+------------------------+------------------------+
| Visibility           | Hard — flow spread     | Clear — flow defined   |
|                      | across many services   | in one place           |
+----------------------+------------------------+------------------------+
| Failure handling     | Each service handles   | Orchestrator handles   |
|                      | its own compensation   | all compensation       |
+----------------------+------------------------+------------------------+
| Best for             | Simple flows with      | Complex flows with     |
|                      | few steps              | many steps and rules   |
+----------------------+------------------------+------------------------+

Designing Compensating Transactions

Not all actions are easily undone. Sending an email cannot be unsent. Charging a card requires a refund, not an undo. Design compensating transactions as business actions, not database rollbacks.

ORIGINAL ACTION         COMPENSATING ACTION
==================      ====================
Create order       -->  Cancel order
Reserve inventory  -->  Release reservation
Charge card        -->  Issue refund
Send email         -->  Send cancellation email (cannot unsend)
Assign delivery    -->  Cancel delivery assignment

Idempotency in Sagas

Network failures during a saga can cause a step to be triggered twice. Each step must be idempotent — executing it twice produces the same result as executing it once.

IDEMPOTENT DESIGN EXAMPLE
===========================
Inventory Service receives "Reserve 2x SKU-005 for ORD-9900" twice
  (network glitch caused duplicate message)

Without idempotency: reserves 4 units total. Wrong.

With idempotency: checks if ORD-9900 reservation already exists.
  If yes: returns "already reserved", does nothing.
  If no: creates new reservation.

Result: always 2 units reserved for ORD-9900. Correct.

Saga State Tracking

The orchestrator (or each service in choreography) tracks the current state of the saga. This state is stored persistently so that if the orchestrator crashes mid-saga, it can resume from where it stopped.

SAGA STATE TABLE
=================
saga_id     | step              | status
ORD-9900    | CreateOrder       | COMPLETED
ORD-9900    | ReserveInventory  | COMPLETED
ORD-9900    | ChargePayment     | FAILED
ORD-9900    | ReleaseInventory  | COMPENSATING
ORD-9900    | CancelOrder       | COMPENSATING

This table persists across service restarts. An orchestrator that crashes and restarts reads this table and continues the saga from the right step.

When to Use the Saga Pattern

  • A business operation spans two or more microservices and must succeed or fail as a unit.
  • Each service owns its own database and cannot participate in a shared database transaction.
  • You need a reliable mechanism to roll back partial changes when a step fails.

For simple single-service operations, Sagas add unnecessary complexity. Use them only when you genuinely need cross-service consistency.

Leave a Comment

Your email address will not be published. Required fields are marked *