Saga Pattern
The pattern actually running in production at most real microservices companies. This is the single most system-design-interview-relevant chapter in this entire book.
A Saga replaces one impossible cross-database transaction with a sequence of local transactions, each fully committed on its own service before the next step runs. There is no global lock, no coordinator holding everyone hostage (Chapter 16 §3) — each step commits independently and immediately. Correctness across the whole sequence comes not from atomicity, but from a second mechanism: if a later step fails, previously completed steps are undone via compensating transactions that run afterward, not a real database rollback.
◆ The problem
Once Payment Service has genuinely committed "card charged," there's no database ROLLBACK that can undo it — that money has already moved. The only way to "undo" a committed real-world action is another, forward-moving action that reverses its effect.
| Step (forward) | Compensating transaction (backward) |
|---|---|
| Create order | Cancel/delete the order |
| Charge payment | Issue a refund |
| Reserve stock | Release the reservation |
| Send confirmation email | Cannot truly be undone — send a "sorry, order cancelled" email instead, or design the saga so the email step runs last, after everything else has already succeeded |
▲ Common mistake
Not every action has a clean compensation (you can't un-send an email, un-deliver a package). Good saga design deliberately orders steps so irreversible actions happen last, after every reversible step has already succeeded — minimizing the window where an un-compensable failure could still occur.
In choreography, each service publishes an event when it finishes its step, and independently reacts to events from other services — directly reusing the event-driven pattern. There is no single service that "knows" the whole saga; the overall flow emerges from each service's local reactions.
Choreography: each service reacts to events independently. Order Service both starts the saga and listens for its own compensation trigger — nobody centrally "runs" the whole flow.
@Component public class OrderSagaListener { @KafkaListener(topics = "payment-failed") public void onPaymentFailed(PaymentFailedEvent event) { // compensation: this service reacts to someone ELSE's failure by undoing its OWN earlier step orderService.cancelOrder(event.orderId()); } }
💻 Code example
@Component public class OrderSagaListener { @KafkaListener(topics = "payment-failed") public void onPaymentFailed(PaymentFailedEvent event) { // compensation: this service reacts to someone ELSE's failure by undoing its OWN earlier step orderService.cancelOrder(event.orderId()); } }
In orchestration, a dedicated orchestrator service explicitly calls each participant in sequence and decides what to do next based on the result — the saga's entire flow logic lives in one place, readable top-to-bottom, rather than scattered as implicit event-reaction chains across every participating service.
@Service public class OrderSagaOrchestrator { public void execute(OrderRequest request) { Long orderId = orderService.createOrder(request); try { paymentService.chargeCard(request.paymentDetails()); } catch (PaymentFailedException e) { orderService.cancelOrder(orderId); // compensate step 1 return; } try { inventoryService.reserveStock(request.items()); } catch (OutOfStockException e) { paymentService.refund(orderId); // compensate step 2 orderService.cancelOrder(orderId); // compensate step 1 return; } notificationService.sendConfirmation(orderId); // irreversible step, deliberately LAST } }
💻 Code example
@Service public class OrderSagaOrchestrator { public void execute(OrderRequest request) { Long orderId = orderService.createOrder(request); try { paymentService.chargeCard(request.paymentDetails()); } catch (PaymentFailedException e) { orderService.cancelOrder(orderId); // compensate step 1 return; } try { inventoryService.reserveStock(request.items()); } catch (OutOfStockException e) { paymentService.refund(orderId); // compensate step 2 orderService.cancelOrder(orderId); // compensate step 1 return; } notificationService.sendConfirmation(orderId); // irreversible step, deliberately LAST } }
| Choreography | Orchestration | |
|---|---|---|
| Flow visibility | Implicit — scattered across every service's event listeners; harder to see the whole picture | Explicit — the entire flow reads top-to-bottom in one orchestrator class |
| Coupling | Loosest — services only know about events, not each other | Orchestrator knows about every participant; participants stay simpler |
| Best for | A small number of steps, simple reactions | Complex, many-step sagas where explicit flow control and visibility matter more than minimal coupling |
| Common real-world choice | Most production systems past 4-5 saga steps lean orchestration — pure choreography's implicit flow becomes genuinely hard to debug and reason about as step count grows. |
◆ The problem
Directly connecting back to Kafka's at-least-once delivery guarantee (covered fully in Chapter 19): a saga step listening for an event can receive that same event more than once, due to consumer restarts or rebalances. If chargeCard() isn't idempotent, a redelivered event means charging the customer's card twice for the same order.
@Transactional public void chargeCard(String orderId, PaymentDetails details) { if (paymentRepository.existsByOrderId(orderId)) { return; // already processed this exact order — safe no-op on redelivery } Payment payment = paymentGateway.charge(details); paymentRepository.save(new PaymentRecord(orderId, payment.getId())); }
💻 Code example
@Transactional public void chargeCard(String orderId, PaymentDetails details) { if (paymentRepository.existsByOrderId(orderId)) { return; // already processed this exact order — safe no-op on redelivery } Payment payment = paymentGateway.charge(details); paymentRepository.save(new PaymentRecord(orderId, payment.getId())); }
Want a visual for this concept?
Generate a diagram tailored to “Saga Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →