DevNotes
Support
Essential Design Patterns · Chapter 11

Saga Pattern

Business consistency across microservices — local transactions and compensating transactions, an order–payment–inventory saga, choreography vs orchestration, idempotency, and why compensation is not rollback.

◷ 7 min read§ 11 sections? 6 interview questions{ } 3 code examples

#Introduction

Purpose: maintain business consistency across multiple services using local transactions and compensating transactions.

In a monolith, creating an order, charging payment and reserving stock can share one database transaction — if any step fails, everything rolls back. In microservices, each service owns its own database, so there's no single transaction spanning them, and distributed two-phase commit (2PC) is slow, fragile and unsupported by most message brokers and cloud databases.

A saga is a sequence of local transactions, one per service. If a step fails, the saga runs compensating transactions that undo the work of the steps that already succeeded.

#Example: Placing an Order

T1 Create Order
T2 Process Payment
T3 Reserve Inventory

If inventory fails:

T3 Reserve Inventory fails
C2 Refund Payment
C1 Cancel Order

Compensations run in reverse order.

The Services

Java
class OrderService {
    public void createOrder()  { System.out.println("Order Created"); }
    public void cancelOrder()  { System.out.println("Order Cancelled"); }
}

class PaymentService {
    public void processPayment() { System.out.println("Payment Completed"); }
    public void refundPayment()  { System.out.println("Payment Refunded"); }
}

class InventoryService {
    public void reserveInventory() {
        System.out.println("Reserving Inventory...");
        throw new RuntimeException("Inventory unavailable");
    }

    public void releaseInventory() {
        System.out.println("Inventory Released");
    }
}

The Saga

Java
class OrderSaga {
    private final OrderService orderService = new OrderService();
    private final PaymentService paymentService = new PaymentService();
    private final InventoryService inventoryService = new InventoryService();

    public void execute() {
        boolean orderCreated = false;
        boolean paymentCompleted = false;
        boolean inventoryReserved = false;

        try {
            orderService.createOrder();            // T1
            orderCreated = true;

            paymentService.processPayment();       // T2
            paymentCompleted = true;

            inventoryService.reserveInventory();   // T3
            inventoryReserved = true;

            System.out.println("Saga Completed Successfully");

        } catch (Exception e) {
            System.out.println("Saga Failed: " + e.getMessage());

            // Compensations are performed in reverse order
            if (inventoryReserved) {
                inventoryService.releaseInventory();
            }
            if (paymentCompleted) {
                paymentService.refundPayment();
            }
            if (orderCreated) {
                orderService.cancelOrder();
            }

            System.out.println("Saga Compensation Completed");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        new OrderSaga().execute();
    }
}
Output
Order Created
Payment Completed
Reserving Inventory...
Saga Failed: Inventory unavailable
Payment Refunded
Order Cancelled
Saga Compensation Completed

Inventory was never reserved (T3 failed), so only C2 and C1 run.

#Choreography

There's no central coordinator. Each service listens for events and reacts by doing its local transaction and publishing the next event:

Order Service — publishes OrderCreated
Payment Service — publishes PaymentCompleted
Inventory Service — publishes InventoryReserved
Shipping Service — ships the order

Memory trick: event → reaction → event → reaction.

On failure, the failing service publishes a failure event (InventoryReservationFailed), and upstream services listen for it and run their compensations (Payment refunds, Order cancels).

#Orchestration

A central orchestrator tells each service what to do and decides what happens next:

  • OrderSaga orchestrator
    • Payment service
    • Inventory service
    • Shipping service
Java
class SagaOrchestrator {
    public void execute() {
        createOrder();
        processPayment();
        reserveInventory();
        createShipment();
    }
}

The orchestrator decides: what happens next? what succeeded? what failed? which compensation should run? In real systems it sends commands (often as messages) and stores the saga's state in a database, so it can resume after a crash.

#Choreography vs Orchestration

ChoreographyOrchestration
Events, no central bossCentral orchestrator sends commands
Loosely coupled — services only know eventsServices are simple; the flow lives in one place
Easy for short flows (2–4 steps)Better for long or complex flows
Hard to see the whole flow; risk of cyclic event dependenciesFlow is explicit, easy to monitor and change
No single point of failureThe orchestrator must be reliable and persistent
Typical tools: Kafka / RabbitMQ eventsTypical tools: Temporal, Camunda, Axon, a custom state machine

#Making Sagas Reliable in Production

The in-memory example shows the idea; real sagas must handle failures between steps:

ConcernSolution
Crashes mid-sagaPersist saga state (orchestrator table or event log) and resume
Duplicate messagesMake every step and compensation idempotent (deduplicate by saga / request ID)
Saving data and publishing an event atomicallyTransactional outbox: write the event to an outbox table in the same local transaction, then relay it to the broker
Compensations that failRetry them (they must be retryable); alert for manual handling as a last resort
Isolation — other requests see intermediate statesSemantic locks / status fields (PENDING_PAYMENT), commutative updates, re-reading values
Steps that can't be undone (sending an email, shipping)Put them last (pivot and retriable transactions), or compensate with a follow-up action (a correction email)

#Saga vs Two-Phase Commit

SagaTwo-phase commit (2PC)
Sequence of local transactions + compensationsOne global atomic transaction
Eventual consistencyStrong consistency (ACID)
No distributed locks — services stay availableParticipants hold locks while waiting for the coordinator
Works with message brokers and NoSQLNeeds XA support from every resource
More application logic (compensations)Blocking, coordinator is a single point of failure

#When to Use

  • A business operation spans several services, each with its own database.
  • Eventual consistency is acceptable, and every step has a meaningful business compensation.
  • Not needed when the operation fits in one service and one database — use a normal local transaction.

#Interview Questions

Q1What is the Saga pattern?

A pattern for maintaining business consistency across multiple services by executing a sequence of local transactions and, if a step fails, running compensating transactions that reverse the steps already completed.

Q2What is a compensating transaction?

A new business operation that semantically undoes an earlier step — for example a refund for a payment, or cancelling an order. It's not a database rollback.

Q3What's the difference between choreography and orchestration?

In choreography, services react to each other's events with no central coordinator. In orchestration, a central orchestrator sends commands to each service and decides the next step and the compensations.

Q4Why not use a distributed transaction (2PC)?

2PC holds locks across services, blocks when the coordinator fails, reduces availability, and isn't supported by many brokers and databases. Sagas trade strong consistency for availability and scalability.

Q5How do you make saga steps reliable?

Idempotent steps and compensations, persisted saga state, the transactional outbox pattern for publishing events, retries for compensations, and monitoring for sagas stuck mid-way.

Q6Does a saga provide ACID guarantees?

It provides atomicity in the business sense (all steps complete or are compensated) but not isolation — intermediate states are visible to other transactions — so it gives eventual consistency.

#Common Mistakes

  • Treating compensation as rollback — the original effects were real and visible.
  • Non-idempotent steps — duplicate messages charge customers twice.
  • Publishing events outside the local transaction — lost or phantom events (use an outbox).
  • No persisted state — a crash leaves the saga half-done forever.
  • Irreversible steps early in the saga.
  • Choreography for long, complex flows that nobody can follow.

#Key Takeaways

  • A saga keeps data consistent across services with local transactions plus compensations.
  • Compensations run in reverse order and are new business transactions, not rollbacks.
  • Choreography = events and no central boss; orchestration = a central coordinator.
  • Production sagas need idempotency, persisted state and the transactional outbox.
  • Sagas give eventual consistency instead of 2PC's blocking strong consistency.