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.
