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 putTwo 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).
| # | Call | Service state after | Saga log append |
|---|---|---|---|
| 1 | createOrder(ORD-1001) | orders[ORD-1001] = PENDING | CREATE_ORDER · STARTED, then DONE |
| 2 | checkAndReduceInventory(SKU-42, 3) | have=1 < 3 → returns false, stock still 1 | RESERVE_INVENTORY · STARTED |
| 3 | — reserve failed — | stock[SKU-42] = 1 (unchanged) | RESERVE_INVENTORY · FAILED |
| 4 | cancelOrder(ORD-1001) (compensation) | orders[ORD-1001] = CANCELLED | CREATE_ORDER · COMPENSATED |
| 5 | return CANCELLED | system consistent: no order, no stock moved | saga 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.
Pitfalls
- Compensation is not rollback. Between step 1 and step 4 the order was visibly
PENDING. A saga has no isolation, so another transaction could read that half-done state (a dirty read) — e.g. a dashboard counts a PENDING order that then vanishes. Guard hot invariants with semantic locks (a PENDING status that other flows refuse to act on) rather than assuming atomic invisibility. - The dual-write trap. "Call the service, then append to the log" (or the reverse) can crash between the two, leaving log and reality disagreeing — an orphaned reservation or a stuck order. Real systems make the state change and the outgoing event one atomic local write via the transactional outbox pattern, then relay the event.
- Non-idempotent steps. Recovery replays the log, and message delivery is at-least-once, so
cancelOrder,addBackInventory, andcreateOrdercan be invoked twice. IfaddBackInventorynaively adds stock every time, replays inflate inventory. Key every action by(sagaId, step)and make it a no-op on repeat. - A compensation that itself fails.
addBackInventoryorcancelOrdercan time out. Compensations must be retried indefinitely with backoff and be idempotent; they are not allowed to "give up", or the system is left inconsistent. - Ephemeral orchestrator state. If the log lives only in memory, an orchestrator crash abandons in-flight sagas — order stuck PENDING, stock reserved forever. The log must be durable and survive the process.
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
- A saga = a chain of local transactions, each with a compensating action; on failure you undo completed steps in reverse, you do not roll back.
- The forward step that mutates a shared resource must be a single atomic operation (compare-and-decrement), or concurrent sagas oversell — a check-then-act version has a TOCTOU race.
- The durable saga log plus idempotent, retryable compensations are what make it crash-recoverable; without them the pattern is a toy.
- Sagas buy availability by giving up isolation — you must actively manage the dirty-read window with semantic locks and other countermeasures.
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.
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.
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.
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.
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.