How to Implement Saga Pattern in Microservices Architecture
To implement the
saga pattern, break a distributed transaction into a series of smaller local transactions across microservices, each with a corresponding compensating transaction to undo changes if needed. Use either choreography (event-based) or orchestration (central coordinator) to manage the flow and ensure eventual consistency.Syntax
The saga pattern involves defining a sequence of local transactions and their compensating transactions. Each local transaction updates the service's database and publishes an event or calls the next service. Compensating transactions undo the previous step if a failure occurs.
Two main approaches:
- Choreography: Each service listens for events and triggers the next step.
- Orchestration: A central coordinator tells each service what to do next.
javascript
class SagaStep { constructor(execute, compensate) { this.execute = execute; // Function to perform local transaction this.compensate = compensate; // Function to undo transaction } } class Saga { constructor(steps) { this.steps = steps; // Array of SagaStep this.currentStep = 0; } async execute() { try { for (; this.currentStep < this.steps.length; this.currentStep++) { await this.steps[this.currentStep].execute(); } } catch (error) { await this.compensate(); throw error; } } async compensate() { for (let i = this.currentStep - 1; i >= 0; i--) { await this.steps[i].compensate(); } } }
Example
This example shows a simple saga for booking a trip: reserving a hotel and booking a flight. If flight booking fails, the hotel reservation is canceled.
javascript
class SagaStep { constructor(execute, compensate) { this.execute = execute; this.compensate = compensate; } } class Saga { constructor(steps) { this.steps = steps; this.currentStep = 0; } async execute() { try { for (; this.currentStep < this.steps.length; this.currentStep++) { await this.steps[this.currentStep].execute(); } console.log('Saga completed successfully'); } catch (error) { console.log('Error occurred:', error.message); await this.compensate(); console.log('Saga compensated'); } } async compensate() { for (let i = this.currentStep - 1; i >= 0; i--) { await this.steps[i].compensate(); } } } // Simulated async operations const reserveHotel = () => new Promise((res) => setTimeout(() => { console.log('Hotel reserved'); res(); }, 100)); const cancelHotel = () => new Promise((res) => setTimeout(() => { console.log('Hotel reservation canceled'); res(); }, 100)); const bookFlight = () => new Promise((res, rej) => setTimeout(() => { console.log('Flight booking failed'); rej(new Error('Flight unavailable')); }, 100)); const cancelFlight = () => new Promise((res) => setTimeout(() => { console.log('Flight booking canceled'); res(); }, 100)); const saga = new Saga([ new SagaStep(reserveHotel, cancelHotel), new SagaStep(bookFlight, cancelFlight) ]); saga.execute();
Output
Hotel reserved
Flight booking failed
Error occurred: Flight unavailable
Hotel reservation canceled
Saga compensated
Common Pitfalls
- Missing compensating transactions: Without them, rollback is impossible, causing data inconsistency.
- Ignoring failure handling: Not handling failures in compensations can leave the system in a bad state.
- Overcomplicating choreography: Too many event listeners can cause hard-to-debug flows.
- Single point of failure in orchestration: The coordinator must be highly available.
javascript
/* Wrong: No compensation defined */ const stepWithoutCompensation = new SagaStep( async () => { console.log('Do something'); }, null ); /* Right: Define compensation to undo changes */ const stepWithCompensation = new SagaStep( async () => { console.log('Do something'); }, async () => { console.log('Undo something'); } );
Quick Reference
Saga Pattern Cheat Sheet:
| Concept | Description |
|---|---|
| Local Transaction | Small transaction in one microservice |
| Compensating Transaction | Undo action for local transaction |
| Choreography | Event-driven saga without central coordinator |
| Orchestration | Central coordinator controls saga steps |
| Event | Message signaling transaction completion |
| Concept | Description |
|---|---|
| Local Transaction | Small transaction in one microservice |
| Compensating Transaction | Undo action for local transaction |
| Choreography | Event-driven saga without central coordinator |
| Orchestration | Central coordinator controls saga steps |
| Event | Message signaling transaction completion |
Key Takeaways
Break distributed transactions into local transactions with compensations to maintain consistency.
Choose choreography for simple event-driven flows or orchestration for centralized control.
Always implement compensating transactions to handle failures and rollback.
Test saga flows thoroughly to avoid partial updates and data inconsistencies.
Ensure the saga coordinator (if used) is reliable and highly available.