đ Monolith to Microservices
Saga Pattern (Choreography & Orchestration)
Distributed transactions across microservices using compensating actions
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 Orchestration: Centralized Controller
Choreography vs Orchestration
| Pattern | How it Works | Pros | Cons |
|---|---|---|---|
| Choreography | Services 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. |
| Orchestration | A 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.