MODULE 1/LESSON 5
🔀 Monolith to Microservices

Saga Pattern (Choreography & Orchestration)

Distributed transactions across microservices using compensating actions

⏱ 18 min📊 Diagram
When a single business operation spans multiple services (e.g., place order → reserve stock → charge payment → send confirmation), you can't use a traditional ACID database transaction across service boundaries. The Saga pattern solves this with a sequence of local transactions and compensating actions.

Key Concepts

Choreography-based Saga

No central coordinator. Each service publishes events and reacts to other services' events. Services are fully autonomous — Order Service publishes 'OrderCreated', Stock Service listens and publishes 'StockReserved'.

Orchestration-based Saga

A central Saga Orchestrator (Temporal, AWS Step Functions) calls each service in sequence. The orchestrator knows the full workflow and triggers compensating actions on failure.

Compensating Transactions

When step N fails, steps N-1 through 1 must be reversed. Compensation is not a rollback — it's a new forward action that semantically undoes a previous action (e.g., 'Release Reserved Stock').

Saga Choreography: Forward & Compensating Flow

Saga Choreography: Forward & Compensating Flow Message Broker (Kafka / RabbitMQ) Order Service Stock Service Payment Service Happy Path (Success) OrderCreated Consumes StockReserved Consumes PaymentSuccess Compensating Path (Rollback on Payment Failure) Payment Service Stock Service Order Service PaymentFailed Consumes StockReleased Consumes OrderCancelled

Saga Orchestration: Centralized Controller

Saga Orchestration (e.g. Temporal, Step Functions) Order Saga Orchestrator Controls the entire workflow Order Service Stock Service Payment Service 1. Create Order Reply OK 2. Reserve Stock Reply OK 3. Process Payment Reply FAIL! 4. Orchestrator triggers Compensation (Release Stock)

Choreography vs Orchestration

PatternHow it WorksProsCons
ChoreographyServices publish and listen to events without a central controller.Loose coupling, no single point of failure, easy to add new listeners.Hard to track the whole flow. Cyclic dependencies can occur. Difficult to debug.
OrchestrationA central controller (e.g. Temporal, Step Functions) commands each service.Clear workflow definition. Easy to handle timeouts and retries. Centralized status.Orchestrator becomes a single point of failure and bottleneck. Higher coupling.

The Outbox Pattern (Crucial for Sagas)

In the code example below, if the database commits successfully but the `eventBus.publish` fails (e.g., Kafka is down), the system is in an inconsistent state. The **Transactional Outbox Pattern** solves this. Instead of directly publishing the event, you write the event to an `outbox` table in the SAME local database transaction. A separate background worker then reads the outbox table and publishes the events reliably.

Choreography Saga — Order Flow with Failure

  • Order Service: Creates order → publishes `OrderCreated` event
  • Stock Service: Consumes `OrderCreated` → reserves inventory → publishes `StockReserved`
  • Payment Service: Consumes `StockReserved` → charges card → if fails, publishes `PaymentFailed`
  • Stock Service: Consumes `PaymentFailed` → releases reserved inventory → publishes `StockReleased`
  • Order Service: Consumes `StockReleased` → marks order as CANCELLED

NestJS Saga Event Handler Example

stock.service.tstypescript
1@EventPattern('OrderCreated')
2async handleOrderCreated(@Payload() event: OrderCreatedEvent) {
3  const session = await this.db.startSession();
4  try {
5    // Local transaction — atomic within this service
6    await this.inventoryRepo.reserveStock(event.items, { session });
7    
8    // Publish next event only after local TX commits
9    await this.eventBus.publish(new StockReservedEvent({
10      orderId: event.orderId,
11      reservationId: uuid(),
12    }));
13    await session.commitTransaction();
14  } catch (err) {
15    await session.abortTransaction();
16    // Trigger compensating action
17    await this.eventBus.publish(new StockReservationFailedEvent({
18      orderId: event.orderId,
19      reason: err.message,
20    }));
21  }
22}
💡
Senior Architect Insight: Choreography works great for simple linear flows (A → B → C). Use Orchestration (Temporal.io) when flows have branching logic, parallel steps, or timeouts — the explicit workflow definition makes debugging distributed failures dramatically easier.