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);
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).
| # | 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, confirm flips the order to CONFIRMED, 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.