Knowledge 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);
            orderService.confirm(order.id);
            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.

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, confirm flips the order to CONFIRMED, 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