The Complete Responsive CMS Blog created by Francesco Malagrino

Saga Pattern in Microservices: Choreography Explained (and Why Compensations Matter)

Category: Software Architecture & Written by Francesco Malagrino On February-20-2026 22:20:29

How to handle distributed “transactions” without 2PC, global locks, or a central coordinator.


In a microservices world, the “all-or-nothing” luxury of a single database transaction is gone.


Placing an order might involve:



  • creating an order record

  • reserving inventory

  • charging a card

  • sending a confirmation

  • scheduling shipping


In a monolith, you might wrap that in BEGIN/COMMIT. In microservices, each step usually lives in a different service, each with its own database. So you can’t rely on a single ACID transaction.


That’s where the Saga Pattern comes in.


What is a Saga?


Saga is a sequence of local transactions. Each local transaction:



  1. updates data within a single service

  2. publishes a message/event to trigger the next step

  3. if something fails later, the saga runs compensating transactions to “undo” earlier work and restore a consistent state


The goal isn’t “perfect rollback” like a database transaction. The goal is consistency — often eventual consistency.


The hardest part isn’t the happy path


The happy path is easy:


OrderCreated → InventoryReserved → PaymentCaptured → OrderConfirmed


Reality is messier:



  • Payment fails after inventory is reserved

  • Inventory reservation fails after the order is created

  • Events get delivered twice

  • One service is down mid-flow


A real saga design is mostly about answering:
“What do we do when step N fails after steps 1..N-1 succeeded?”


That answer is compensation.


Saga choreography: coordination without a “brain”


There are two common saga coordination styles: choreography and orchestration.


This article is about choreography.


With choreography:



  • there is no central coordinator

  • each service reacts to events

  • each service decides what to do next


It’s decentralized and can scale nicely — but you must be disciplined or you end up with “event spaghetti.”


A choreography flow (example)


Let’s say we have three services:



  • Order Service

  • Inventory Service

  • Payment Service


Happy path



  1. Order Service creates the order and emits:


type OrderCreated = {
type: "OrderCreated";
orderId: string;
items: Array<{ sku: string; qty: number }>;
totalPrice: number;
};

2) Inventory Service listens for OrderCreated, reserves stock, emits InventoryReserved


3) Payment Service listens for InventoryReserved, charges the card, emits PaymentCaptured


4) Order Service listens for PaymentCaptured, marks the order CONFIRMED


No service is “in charge.” The workflow emerges from event reactions.


The “undo”: compensating transactions


Now the failure case:



  • Inventory was reserved ✅

  • Payment fails ❌


Payment emits:


type PaymentFailed = {
type: "PaymentFailed";
orderId: string;
reason: "INSUFFICIENT_FUNDS" | "PROCESSOR_DOWN";
};

Inventory listens for PaymentFailed and compensates by releasing stock:


async function onPaymentFailed(evt: PaymentFailed) {
await inventory.releaseReservation(evt.orderId);
// optionally emit InventoryReleased
}

Order listens and updates state to CANCELLED (and maybe notifies the user).


Important nuance


Compensation is application-specific and can fail too — so compensation steps should also be idempotent (safe to retry).


What makes choreography succeed (not just “work in a demo”)


1) Idempotency (duplicates happen)


Most event systems deliver messages at least once. Your handler must tolerate duplicates:



  • store a processed event id / idempotency key

  • if you’ve already processed it, do nothing


2) Correlation IDs (trace a single order everywhere)


Every event should include orderId (and often a sagaId) so you can trace the whole flow in logs.


3) Retries + DLQ


Transient failures retry. Poison messages go to a dead-letter queue.


4) Timeouts


If InventoryReserved never arrives, you need a timeout rule:



  • cancel order after X minutes

  • release reservation after X minutes


Without timeouts, your saga can hang forever.


When choreography is a great choice


Choreography shines when:



  • the workflow is relatively small and stable

  • teams want high autonomy

  • you want to avoid a central coordinator service


But as workflows grow, many teams switch to orchestration to simplify observability and control flow.


Up next


In Part 2, I’ll deep dive into my own saga project: an orchestration-based implementation in Node.js/TypeScript that makes retries, timeouts, idempotency, and rollback logic explicit.


Share


Comments

Share your thoughts about this post