Skip to content

Saga Pattern — Distributed Transactions in Microservices Guide

DodaTech Updated 2026-06-24 5 min read

In this tutorial, you'll learn about Saga Pattern. We cover key concepts, practical examples, and best practices.

The saga pattern manages distributed transactions across microservices by breaking them into a sequence of local transactions with compensating actions for rollback, ensuring data consistency without distributed locking.

Why the Saga Pattern Matters

In a monolith, a database transaction can atomically update orders, inventory, and payments. In microservices, each service owns its database — no single transaction spans them. Two-phase commit (2PC) is slow, fragile, and doesn't work across heterogeneous databases. The saga pattern is the practical alternative. Every major e-commerce platform — Amazon, eBay, Uber — uses sagas for order processing. Doda Browser's premium subscription flow uses an orchestrated saga: create subscription, charge payment, activate features, and notify the user, with compensating steps at each stage.

Plain-Language Explanation

Think of booking a flight + hotel + car for a vacation. You call the airline (reserve seats), then the hotel (reserve room), then the rental car company (reserve car). If the car company has no availability, you don't just leave the airline and hotel reservations hanging — you call them back to cancel. Each call is a local transaction. The cancellations are compensating transactions. That's a saga.

graph TD
    subgraph "Orchestrated Saga"
        ORC[Saga Orchestrator]
        ORC -->|1. Reserve Inventory| INV[Inventory Service]
        ORC -->|2. Process Payment| PAY[Payment Service]
        ORC -->|3. Confirm Order| ORD[Order Service]
        PAY -->|Failure| COMP1[Compensate: Release Inventory]
        INV -->|Failure| COMP2[Compensate: Cancel Order]
    end
    style ORC fill:#3498db,color:#fff
    style INV fill:#27ae60,color:#fff
    style PAY fill:#e67e22,color:#fff
    style ORD fill:#9b59b6,color:#fff

Choreography Saga with Kafka

Services react to events and emit their own — no central coordinator:

import json, time

class ChoreographySaga:
    def __init__(self, event_bus):
        self.bus = event_bus

    def place_order(self, order_id: str, user_id: str, amount: float):
        self.bus.publish("OrderPlaced", {"order_id": order_id, "user_id": user_id, "amount": amount})

class InventoryService:
    def handle_order_placed(self, event: dict):
        print(f"Reserving inventory for order {event['order_id']}")
        self.bus.publish("InventoryReserved", event)

class PaymentService:
    def handle_inventory_reserved(self, event: dict):
        print(f"Processing payment of ${event['amount']}")
        self.bus.publish("PaymentProcessed", event)

class OrderService:
    def handle_payment_processed(self, event: dict):
        print(f"Confirming order {event['order_id']}")

Orchestrated Saga with Python

A central orchestrator coordinates all steps and compensations:

class OrchestratedSaga:
    def __init__(self):
        self.steps = []
        self.compensations = []

    def add_step(self, name: str, action, compensate):
        self.steps.append((name, action, compensate))

    def execute(self, ctx: dict):
        completed = []
        try:
            for name, action, _ in self.steps:
                print(f"Executing: {name}")
                action(ctx)
                completed.append(name)
            print("Saga completed successfully")
        except Exception as e:
            print(f"Failed at {name}: {e}")
            for step_name, _, comp in reversed(self.steps[:len(completed)]):
                print(f"Compensating: {step_name}")
                try:
                    comp(ctx)
                except Exception as ce:
                    print(f"Compensation failed for {step_name}: {ce}")

class OrderSaga:
    def create(self, ctx: dict):
        saga = OrchestratedSaga()
        saga.add_step("ReserveInventory", self.reserve, self.release)
        saga.add_step("ProcessPayment", self.pay, self.refund)
        saga.add_step("ConfirmOrder", self.confirm, self.cancel)
        saga.execute(ctx)

    def reserve(self, ctx): print(f"  Reserved inventory")
    def release(self, ctx): print(f"  Released inventory")
    def pay(self, ctx): raise Exception("Insufficient funds")
    def refund(self, ctx): print(f"  Refunded payment")
    def confirm(self, ctx): print(f"  Confirmed order")
    def cancel(self, ctx): print(f"  Cancelled order")

OrderSaga().create({"order_id": "O123"})

Expected output:

Executing: ReserveInventory
  Reserved inventory
Executing: ProcessPayment
  Insufficient funds
Compensating: ProcessPayment
  Refunded payment
Compensating: ReserveInventory
  Released inventory

Saga State Persistence

A saga must survive service restarts. Store saga state in a database:

CREATE TABLE saga_state (
    saga_id VARCHAR(64) PRIMARY KEY,
    saga_type VARCHAR(128),
    current_step INT,
    status VARCHAR(20),
    context JSONB,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);
class PersistentSaga:
    def load(self, saga_id: str) -> dict:
        return self.db.query("SELECT * FROM saga_state WHERE saga_id = :sid", {"sid": saga_id})

    def save(self, state: dict):
        self.db.execute(
            "INSERT INTO saga_state (saga_id, saga_type, current_step, status, context) "
            "VALUES (:id, :type, :step, :status, :ctx) "
            "ON CONFLICT (saga_id) DO UPDATE SET current_step = :step, status = :status",
            state
        )

Common Mistakes

  1. No compensating transactions: Every step must have a working compensation. An unrevertable step (like sending an email) should be the last step.

  2. Synchronous sagas: Blocking on each step couples services. Use async communication (Kafka, RabbitMQ) for each step.

  3. Missing idempotency: Service restarts may cause duplicate saga steps. Each step should be idempotent — executing twice produces the same result.

  4. No saga state persistence: If the orchestrator crashes, in-flight sagas are lost. Persist state after each step for recovery.

  5. Ignoring partial failures: A compensation might fail. Implement a manual retry or dead-letter queue for failed compensations.

Practice Questions

  1. What problem does the saga pattern solve? Distributed transactions across microservices without distributed locking or two-phase commit, using compensating actions for rollback.

  2. Choreography vs orchestration: which is better? Choreography is simpler for few services but gets complex with many. Orchestration centralizes coordination but adds a single point of failure. Start with orchestration.

  3. What is a compensating transaction? An action that undoes a previous step in the saga. For example, refunding a payment or releasing reserved inventory.

  4. How do you handle saga failures? Persist saga state, retry failed steps with exponential backoff, and send failed compensations to a dead-letter queue for manual resolution.

  5. Can a saga be eventually consistent? Yes. Sagas provide eventual consistency. The system converges to a consistent state once all steps or compensations complete.

Mini Project

Build a travel booking saga:

class TravelSaga:
    def __init__(self):
        self.booked = []

    def book_flight(self, flight: str):
        if not flight:
            raise Exception("No flight available")
        self.booked.append(("flight", flight))
        return True

    def cancel_flight(self, flight: str):
        self.booked.remove(("flight", flight))
        return True

    def book_hotel(self, hotel: str):
        if not hotel:
            raise Exception("No hotel available")
        self.booked.append(("hotel", hotel))
        return True

    def cancel_hotel(self, hotel: str):
        self.booked.remove(("hotel", hotel))
        return True

    def book_trip(self, flight: str, hotel: str):
        steps = [
            (lambda: self.book_flight(flight), lambda: self.cancel_flight(flight)),
            (lambda: self.book_hotel(hotel), lambda: self.cancel_hotel(hotel)),
        ]
        done = []
        for i, (action, compensate) in enumerate(steps):
            try:
                action()
                done.append(i)
            except Exception as e:
                print(f"Failed: {e}")
                for j in reversed(done):
                    steps[j][1]()
                print("Trip booking rolled back")
                return
        print(f"Trip booked: flight={flight}, hotel={hotel}")

TravelSaga().book_trip("AA123", "")
TravelSaga().book_trip("AA123", "Hilton")

Expected output:

Failed: No hotel available
Trip booking rolled back
Trip booked: flight=AA123, hotel=Hilton

Cross-References

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro