Placing an order might involve:
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.
A Saga is a sequence of local transactions. Each local transaction:
The goal isn’t “perfect rollback” like a database transaction. The goal is consistency — often eventual consistency.
The happy path is easy:
OrderCreated → InventoryReserved → PaymentCaptured → OrderConfirmed
Reality is messier:
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.
There are two common saga coordination styles: choreography and orchestration.
This article is about choreography.
With choreography:
It’s decentralized and can scale nicely — but you must be disciplined or you end up with “event spaghetti.”
Let’s say we have three services:
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.
Now the failure case:
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).
Compensation is application-specific and can fail too — so compensation steps should also be idempotent (safe to retry).
Most event systems deliver messages at least once. Your handler must tolerate duplicates:
Every event should include orderId (and often a sagaId) so you can trace the whole flow in logs.
Transient failures retry. Poison messages go to a dead-letter queue.
If InventoryReserved never arrives, you need a timeout rule:
Without timeouts, your saga can hang forever.
Choreography shines when:
But as workflows grow, many teams switch to orchestration to simplify observability and control flow.
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.
July-27-2026 14:00:48
July-15-2026 14:39:41
July-15-2026 14:35:10