0
0
MicroservicesHow-ToIntermediate ยท 4 min read

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:

ConceptDescription
Local TransactionSmall transaction in one microservice
Compensating TransactionUndo action for local transaction
ChoreographyEvent-driven saga without central coordinator
OrchestrationCentral coordinator controls saga steps
EventMessage signaling transaction completion
ConceptDescription
Local TransactionSmall transaction in one microservice
Compensating TransactionUndo action for local transaction
ChoreographyEvent-driven saga without central coordinator
OrchestrationCentral coordinator controls saga steps
EventMessage 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.