The Saga Pattern Unleashed: A Deep Dive into Distributed Transactions with TypeScript

Krzysztof Słomka
4 min readAug 23, 2023

--

Introduction

Building modern distributed systems requires solutions to ensure data consistency across services without relying on traditional database transactions. The Saga Pattern emerges as a key approach to managing such complex, long-running transactions. This article explores the Saga Pattern, presenting both Choreography and Orchestration implementations while delving into the nuances through extensive TypeScript examples.

Understanding the Saga Pattern

A Saga is a sequence of local transactions coordinated to ensure a global outcome. It can be executed in two distinct ways:

  • Orchestration: Guided by a central orchestrator.
  • Choreography: Decentralized, with each transaction triggering the next.

Orchestration in TypeScript

Here’s a detailed orchestration example, focusing on handling an online order:

Step 1: Create Order

private createOrder(orderId: string) {
// Logic to create an order
console.log(`Creating order ${orderId}`);
}

Step 2: Reserve Stock

private reserveStock(orderId: string) {
// Logic to reserve stock
console.log(`Reserving stock for order ${orderId}`);
}

Step 3: Process Payment

private processPayment(orderId: string) {
// Logic to process payment
console.log(`Processing payment for order ${orderId}`);
}

Step 4: Ship OrdertypescriptCopy code

private shipOrder(orderId: string) {
// Logic to ship the order
console.log(`Shipping order ${orderId}`);
}

Step 5: Handle Compensation

private compensateOrder(orderId: string, error: Error) {
// Logic to rollback the order
console.log(`Order compensation due to ${error.message}`);
}

Orchestration Class

class OrderSaga {
start(orderId: string) {
try {
this.createOrder(orderId);
this.reserveStock(orderId);
this.processPayment(orderId);
this.shipOrder(orderId);
console.log('Order processed successfully');
} catch (error) {
this.compensateOrder(orderId, error);
}
}
// ... Rest of the methods
}
const saga = new OrderSaga();
saga.start('12345');

Choreography in TypeScript

Choreography relies on an event-driven approach, where each local transaction triggers the next through events:

import { EventEmitter } from 'events';
const sagaEvents = new EventEmitter();
// Function definitions
function createOrder(orderId: string) { /*...*/ }
function reserveStock(orderId: string) { /*...*/ }
function processPayment(orderId: string) { /*...*/ }
function shipOrder(orderId: string) { /*...*/ }
function compensateOrder(orderId: string) { /*...*/ }
// Event handling
sagaEvents.on('OrderCreated', reserveStock);
sagaEvents.on('StockReserved', processPayment);
sagaEvents.on('PaymentProcessed', shipOrder);
sagaEvents.on('OrderFailed', compensateOrder);
// Start the saga
createOrder('12345');

Why Choose the Saga Pattern?

  • Reliability: Ensures data consistency across services.
  • Scalability: Supports complex, long-running transactions.
  • Flexibility: Can be implemented in a centralized or decentralized manner.

Challenges and Their Solutions

  1. Failure Handling: Design compensating transactions to undo changes.
  2. Complexity Management: Utilize orchestration or tools like state machines.
  3. Idempotency: Make operations idempotent to prevent repeated effects.

Real-World Applications in Fintech

The Saga Pattern finds wide application in industries like fintech, as we’ve explored in previous posts like scaling PostgreSQL and microservices.

Make Saga generic

Creating a generic Saga implementation in TypeScript requires defining a common structure that can be reused across different use cases. A generic Saga implementation will typically involve an orchestrator or manager that coordinates different steps or transactions within the saga.

Here’s a generic Saga implementation you can adapt to various scenarios:

Step 1: Define the Saga Step Interface

Each step in the Saga represents a local transaction. We’ll define an interface for the steps, so they all have a common structure.

interface SagaStep {
execute(): Promise<void>;
compensate(): Promise<void>;
}

Step 2: Implement a Saga Orchestrator

The orchestrator will manage the execution and compensation of the Saga steps.

class SagaOrchestrator {
private steps: SagaStep[] = [];

addStep(step: SagaStep) {
this.steps.push(step);
}

async execute() {
for (const step of this.steps) {
try {
await step.execute();
} catch (error) {
await this.compensate();
throw error;
}
}
}

private async compensate() {
for (const step of this.steps.reverse()) {
await step.compensate();
}
}
}

Step 3: Define Specific Steps

Each step in the Saga implements the SagaStep interface. Here's an example:

class OrderCreationStep implements SagaStep {
async execute() {
// Logic for creating the order
}
async compensate() {
// Logic for rolling back the order creation
}
}

class PaymentProcessingStep implements SagaStep {
async execute() {
// Logic for processing the payment
}
async compensate() {
// Logic for refunding or canceling the payment
}
}

Step 4: Assemble and Execute the Saga

Now, you can assemble the Saga by adding specific steps and executing it.

const saga = new SagaOrchestrator();

saga.addStep(new OrderCreationStep());
saga.addStep(new PaymentProcessingStep());

saga.execute()
.then(() => console.log('Saga executed successfully'))
.catch((error) => console.log(`Saga execution failed: ${error}`));

This generic Saga implementation allows you to define any series of steps and coordinate their execution and compensation. You can create different steps to model different business processes and use the SagaOrchestrator class to manage their execution.

This pattern adds flexibility to your code, making it easier to add, remove, or modify steps in a Saga without having to change the core logic that manages the Saga’s execution. It also promotes the separation of concerns by isolating the logic of each step from the overall management of the Saga. It can be quite handy in building scalable and maintainable systems, much like the other patterns and practices we’ve discussed previously.

Conclusion

The Saga Pattern is an essential tool for developing modern distributed systems, especially with complex transactions. By understanding its principles and studying real-world code examples in TypeScript, developers can build robust, scalable applications.

Whether you choose to orchestrate or choreograph your saga, the pattern’s versatility ensures consistency, flexibility, and reliability. By embracing the Saga Pattern, developers can navigate the challenges of distributed transactions and contribute to building a future of interconnected and sophisticated digital systems.

These extensive TypeScript code examples offer practical insights into implementing the Saga Pattern, laying a firm foundation for those venturing into complex transaction handling within modern application development. With this knowledge, you are empowered to take on intricate challenges in distributed system development with confidence and skill. Feel free to explore earlier posts for more insights into related topics!

--

--

Krzysztof Słomka

My name is Krzysztof, I'm a software architect and developer, with experience of leading teams and delivering large scalable projects for over 13 years...