CMD Guide
HomeSystem DesignMicroservices Patterns

Saga Pattern A Example

A saga replaces the one big ACID transaction you cannot have across services with a sequence of local ACID transactions, each committed in its own service's database, plus a matching compensating transaction for every step so that a failure midway can be semantically undone by walking the completed steps backwards.

The mechanism in one orchestrated flow

We keep the three actors the shallow version had — an OrderService, an InventoryService, and an OrderOrchestrator that drives them — but now every method has a real body, the reserve step is atomic, and the orchestrator writes a durable saga log so a crash mid-flight is recoverable. Each service owns its own data (database-per-service); no shared transaction spans them.

The order object and its states:

enum OrderStatus { PENDING, CONFIRMED, CANCELLED }
enum StepState  { STARTED, DONE, FAILED, COMPENSATED }

class Order {
    final String id, customerId, sku;
    final int qty;
    OrderStatus status;
    Order(String id, String customerId, String sku, int qty) {
        this.id = id; this.customerId = customerId; this.sku = sku; this.qty = qty;
    }
}

The two services

OrderService exposes a forward action (createOrder) and its compensation (cancelOrder). Both are local, single-database writes:

public class OrderService {
    private final Map<String, Order> orders = new ConcurrentHashMap<>();

    public void createOrder(Order order) {          // forward
        order.status = OrderStatus.PENDING;
        orders.put(order.id, order);                // local ACID commit
    }

    public void confirm(String orderId) {
        Order o = orders.get(orderId);
        if (o != null) o.status = OrderStatus.CONFIRMED;
    }

    public void cancelOrder(String orderId) {       // compensation for createOrder
        Order o = orders.get(orderId);
        if (o != null) o.status = OrderStatus.CANCELLED;   // idempotent
    }
}

InventoryService reserves stock. The reservation and its compensation are the interesting part — the reduce must be a single atomic compare-and-decrement, not a read followed by a write:

public class InventoryService {
    private final Map<String, Integer> stock = new ConcurrentHashMap<>();

    public boolean checkAndReduceInventory(String sku, int qty) {   // forward
        boolean[] reserved = {false};
        stock.computeIfPresent(sku, (k, have) -> {
            if (have >= qty) { reserved[0] = true; return have - qty; }
            return have;                              // not enough: leave unchanged
        });
        return reserved[0];
    }

    public void addBackInventory(String sku, int qty) {             // compensation
        stock.merge(sku, qty, Integer::sum);
    }
}

Why the naive reserve is wrong

The obvious version reads the count, checks it, then writes the reduced count in a second step:

// BROKEN: check-then-act
int have = stock.get(sku);
if (have >= qty) stock.put(sku, have - qty);   // race window between get and put

Two concurrent orders for the last unit both read have = 1, both pass the check, and both write 0 — you have oversold. This is a time-of-check-to-time-of-use (TOCTOU) race. Because a saga step commits independently and there is no surrounding transaction to serialize the two callers, the whole reserve must collapse into one atomic operation on the row (here computeIfPresent; in SQL, UPDATE stock SET qty = qty - :n WHERE sku = :sku AND qty >= :n and check the affected-row count).

The orchestrator and its saga log

The orchestrator issues forward calls, records each step's state in a durable log before and after the call, and on failure runs the compensations of the already-completed steps in reverse order:

public class OrderOrchestrator {
    private final OrderService orderService;
    private final InventoryService inventoryService;
    private final SagaLog log;      // durable, append-only

    public OrderOrchestrator(OrderService os, InventoryService is, SagaLog log) {
        this.orderService = os; this.inventoryService = is; this.log = log;
    }

    public OrderStatus processOrder(Order order) {
        String sagaId = order.id;

        log.append(sagaId, "CREATE_ORDER", StepState.STARTED);
        orderService.createOrder(order);
        log.append(sagaId, "CREATE_ORDER", StepState.DONE);

        log.append(sagaId, "RESERVE_INVENTORY", StepState.STARTED);
        boolean reserved = inventoryService.checkAndReduceInventory(order.sku, order.qty);

        if (reserved) {
            log.append(sagaId, "RESERVE_INVENTORY", StepState.DONE);
            log.append(sagaId, "CONFIRM_ORDER", StepState.STARTED);
            orderService.confirm(order.id);
            log.append(sagaId, "CONFIRM_ORDER", StepState.DONE);
            return OrderStatus.CONFIRMED;
        }

        // reserve failed: compensate completed steps in REVERSE order
        log.append(sagaId, "RESERVE_INVENTORY", StepState.FAILED);
        orderService.cancelOrder(order.id);                       // undo CREATE_ORDER
        log.append(sagaId, "CREATE_ORDER", StepState.COMPENSATED);
        return OrderStatus.CANCELLED;
    }
}

On restart after a crash, the orchestrator scans the log for the newest saga whose steps are not all DONE or COMPENSATED; a STARTED with no terminal state tells it exactly where to resume rolling forward or backward. That is why the log is the backbone, not an exercise.

Note confirm is bracketed too — otherwise a crash between the reserve commit and the confirm leaves a saga the log calls finished but the order table calls PENDING; every state mutation the orchestrator drives must be a logged step, or recovery is blind to it.

Worked trace: an order that cannot be filled

Inputs: Order ORD-1001, customer C-77, SKU-42 ("Wireless Mouse"), qty = 3. Inventory currently holds stock[SKU-42] = 1. We call orchestrator.processOrder(ORD-1001).

#CallService state afterSaga log append
1createOrder(ORD-1001)orders[ORD-1001] = PENDINGCREATE_ORDER · STARTED, then DONE
2checkAndReduceInventory(SKU-42, 3)have=1 < 3 → returns false, stock still 1RESERVE_INVENTORY · STARTED
3— reserve failed —stock[SKU-42] = 1 (unchanged)RESERVE_INVENTORY · FAILED
4cancelOrder(ORD-1001) (compensation)orders[ORD-1001] = CANCELLEDCREATE_ORDER · COMPENSATED
5return CANCELLEDsystem consistent: no order, no stock movedsaga terminal

Contrast the happy path: had stock been 5, step 2 atomically drops it to 2, logs RESERVE_INVENTORY · DONE, then CONFIRM_ORDER · STARTED, confirm flips the order to CONFIRMED, CONFIRM_ORDER · DONE is appended, and no compensation runs.

diagram
diagram

Pitfalls

When to use it / when NOT to

Reach for a saga when a single business operation must update state in two or more services that each own a separate database, the operation may be long-running, and you can define a sensible business undo for each step (cancel the order, restock the item, refund the card). The concrete signal: you find yourself wanting one transaction to span two databases and there is no such thing.

Orchestration vs. choreography. This page uses orchestration — a central coordinator that explicitly calls each step. You gain a single place to read the flow, monitor progress, and drive recovery; you pay with a coordinator that is another moving part and can bloat into a god-object that knows every service's business rules. Choreography instead has each service react to events the previous one emitted — maximally decoupled and no central dependency, but the end-to-end flow is emergent and painful to trace or debug across teams. Choose orchestration when the workflow has real branching, timeouts, and needs observability; prefer choreography when the flow is short, linear, and you value autonomy over a legible control point.

Saga vs. two-phase commit (2PC/XA). 2PC gives you true atomic isolation across resources, so no one ever sees a half-done state — but it is a blocking protocol: locks are held from prepare through commit, a stalled participant or a network partition freezes everyone, and it demands an XA transaction coordinator with every store playing along. That trades availability for isolation, which is exactly backwards for internet-scale microservices. A saga trades isolation for availability: no locks held across services, each step commits immediately, and failures are handled by compensation instead of by blocking. Choose a saga when services are heterogeneous, transactions are long-lived, and high availability under partition matters. Prefer 2PC when there are only a couple of XA-capable resources in one datacenter, transactions are short, and you genuinely need strong isolation (some financial settlement). Prefer neither — a single local ACID transaction — when the data can live in one database (a modular monolith); do not adopt saga complexity to solve a problem you created by splitting services prematurely.

Takeaways


Re-authored and deepened for this guide. Sources: Chris Richardson, Microservices Patterns (Manning, 2018), ch. 4–5 (Saga, orchestration vs. choreography, countermeasures) and microservices.io (Saga, Transactional Outbox); Hector Garcia-Molina & Kenneth Salem, "Sagas," ACM SIGMOD, 1987 (the original concept); the 2PC comparison follows the standard treatment of XA/blocking commit protocols.

🤖 Don't fully get this? Learn it with Claude

Stuck on Saga Pattern A Example? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Saga Pattern A Example** (System Design) and want to truly understand it. Explain Saga Pattern A Example from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Saga Pattern A Example** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Saga Pattern A Example** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Saga Pattern A Example** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes