# Distributed Transactions: Saga Pattern

# Distributed Transactions: Saga Pattern

## Metadata

**SEO Title:** Distributed Transactions: Saga Pattern Guide for Developers

**Meta Description:** Master the Saga pattern for distributed transactions in microservices. Learn TypeScript implementation, common pitfalls, best practices, and solutions to coordination challenges.

**Keywords:** saga pattern, distributed transactions, microservices, event-driven architecture, compensating transactions, orchestration, choreography, TypeScript microservices

**Tags:** microservices, distributed-systems, saga-pattern, event-driven, typescript, transactions, architecture

---

## The Problem: Why Distributed Transactions Are Hard

In monolithic applications, maintaining data consistency is straightforward. You wrap operations in a database transaction, and either everything commits or everything rolls back. ACID guarantees handle the complexity for you. But when you decompose a monolith into microservices, each with its own database, this elegant simplicity shatters.

Consider an e-commerce system split into Order Service, Payment Service, Inventory Service, and Shipping Service. When a customer places an order, you need to:

1. Create an order record
2. Process the payment
3. Reserve inventory
4. Schedule shipping

In a monolith, you'd wrap these in a single transaction. In a distributed system, each service owns its data. You can't use a traditional ACID transaction across service boundaries because:

**Network Unreliability**: Services communicate over networks that can fail, timeout, or deliver messages out of order. A service might complete its operation but fail to acknowledge it, leaving the system in an uncertain state.

**Autonomy Requirements**: Microservices should be independently deployable and scalable. Locking resources across services creates tight coupling and defeats the purpose of distributed architecture.

**Performance Bottlenecks**: Distributed transactions using protocols like two-phase commit (2PC) require coordination and locks across services. This creates contention, reduces throughput, and makes the system vulnerable to cascading failures. If one service is slow or unavailable, it blocks all others.

**Heterogeneous Data Stores**: Modern microservices often use different databases optimized for their specific needs—PostgreSQL for orders, MongoDB for product catalogs, Redis for caching. Not all support distributed transaction protocols.

**Partial Failures**: The most insidious problem. Payment succeeds, but inventory reservation fails. Now you've charged a customer for items you can't deliver. Or inventory is reserved, but payment fails, leaving items locked unnecessarily.

The CAP theorem reminds us that in the presence of network partitions, we must choose between consistency and availability. Most modern systems choose availability, accepting eventual consistency. But "eventual" doesn't mean "automatic"—you need a mechanism to orchestrate operations and handle failures gracefully.

This is where the Saga pattern emerges as a pragmatic solution. Instead of trying to maintain ACID properties across services, Sagas break long-running transactions into a series of local transactions, each with a compensating action that can undo its effects if something goes wrong downstream.

---

## Modern TypeScript Solution

The Saga pattern comes in two flavors: **Orchestration** (centralized coordinator) and **Choreography** (decentralized event-driven). Let's implement an orchestration-based saga using TypeScript.

### Core Saga Infrastructure

```typescript
// saga-step.ts
interface SagaStep<T = any, R = any> {
  name: string;
  action: (context: T) => Promise<R>;
  compensation: (context: T, result?: R) => Promise<void>;
}

interface SagaContext {
  orderId: string;
  userId: string;
  amount: number;
  paymentId?: string;
  inventoryReservationId?: string;
  shippingId?: string;
}

class SagaExecutor {
  private completedSteps: Array<{
    step: SagaStep;
    result: any;
  }> = [];

  async execute(
    steps: SagaStep[],
    context: SagaContext
  ): Promise<{ success: boolean; error?: Error }> {
    try {
      for (const step of steps) {
        console.log(`Executing step: ${step.name}`);
        const result = await step.action(context);
        this.completedSteps.push({ step, result });
        
        // Update context with result
        Object.assign(context, result);
      }
      return { success: true };
    } catch (error) {
      console.error(`Step failed: ${error.message}`);
      await this.compensate(context);
      return { success: false, error: error as Error };
    }
  }

  private async compensate(context: SagaContext): Promise<void> {
    console.log('Starting compensation...');
    
    // Execute compensations in reverse order
    for (let i = this.completedSteps.length - 1; i >= 0; i--) {
      const { step, result } = this.completedSteps[i];
      try {
        console.log(`Compensating step: ${step.name}`);
        await step.compensation(context, result);
      } catch (error) {
        console.error(`Compensation failed for ${step.name}:`, error);
        // Log to dead letter queue for manual intervention
        await this.logCompensationFailure(step.name, context, error);
      }
    }
  }

  private async logCompensationFailure(
    stepName: string,
    context: SagaContext,
    error: any
  ): Promise<void> {
    // Send to monitoring/alerting system
    console.error('CRITICAL: Compensation failure', {
      stepName,
      context,
      error,
    });
  }
}
```

### Implementing the Order Saga

```typescript
// order-saga.ts
class OrderService {
  async createOrder(userId: string, amount: number): Promise<string> {
    // Simulate order creation
    const orderId = `order_${Date.now()}`;
    console.log(`Order created: ${orderId}`);
    return orderId;
  }

  async cancelOrder(orderId: string): Promise<void> {
    console.log(`Order cancelled: ${orderId}`);
  }
}

class PaymentService {
  async processPayment(
    userId: string,
    amount: number
  ): Promise<string> {
    // Simulate payment processing
    if (Math.random() > 0.8) {
      throw new Error('Payment declined');
    }
    const paymentId = `payment_${Date.now()}`;
    console.log(`Payment processed: ${paymentId}`);
    return paymentId;
  }

  async refundPayment(paymentId: string): Promise<void> {
    console.log(`Payment refunded: ${paymentId}`);
  }
}

class InventoryService {
  async reserveInventory(orderId: string): Promise<string> {
    if (Math.random() > 0.9) {
      throw new Error('Insufficient inventory');
    }
    const reservationId = `reservation_${Date.now()}`;
    console.log(`Inventory reserved: ${reservationId}`);
    return reservationId;
  }

  async releaseInventory(reservationId: string): Promise<void> {
    console.log(`Inventory released: ${reservationId}`);
  }
}

class ShippingService {
  async scheduleShipping(orderId: string): Promise<string> {
    const shippingId = `shipping_${Date.now()}`;
    console.log(`Shipping scheduled: ${shippingId}`);
    return shippingId;
  }

  async cancelShipping(shippingId: string): Promise<void> {
    console.log(`Shipping cancelled: ${shippingId}`);
  }
}

// Saga definition
const orderService = new OrderService();
const paymentService = new PaymentService();
const inventoryService = new InventoryService();
const shippingService = new ShippingService();

const createOrderSaga: SagaStep<SagaContext>[] = [
  {
    name: 'CreateOrder',
    action: async (ctx) => ({
      orderId: await orderService.createOrder(ctx.userId, ctx.amount),
    }),
    compensation: async (ctx) => {
      if (ctx.orderId) await orderService.cancelOrder(ctx.orderId);
    },
  },
  {
    name: 'ProcessPayment',
    action: async (ctx) => ({
      paymentId: await paymentService.processPayment(
        ctx.userId,
        ctx.amount
      ),
    }),
    compensation: async (ctx) => {
      if (ctx.paymentId) await paymentService.refundPayment(ctx.paymentId);
    },
  },
  {
    name: 'ReserveInventory',
    action: async (ctx) => ({
      inventoryReservationId: await inventoryService.reserveInventory(
        ctx.orderId
      ),
    }),
    compensation: async (ctx) => {
      if (ctx.inventoryReservationId) {
        await inventoryService.releaseInventory(ctx.inventoryReservationId);
      }
    },
  },
  {
    name: 'ScheduleShipping',
    action: async (ctx) => ({
      shippingId: await shippingService.scheduleShipping(ctx.orderId),
    }),
    compensation: async (ctx) => {
      if (ctx.shippingId) {
        await shippingService.cancelShipping(ctx.shippingId);
      }
    },
  },
];

// Usage
async function placeOrder(userId: string, amount: number) {
  const executor = new SagaExecutor();
  const context: SagaContext = { orderId: '', userId, amount };
  
  const result = await executor.execute(createOrderSaga, context);
  
  if (result.success) {
    console.log('Order completed successfully:', context);
  } else {
    console.log('Order failed and was compensated:', result.error?.message);
  }
}
```

---

## Common Pitfalls

**Non-Idempotent Operations**: Network retries can cause actions or compensations to execute multiple times. Always design operations to be idempotent. Use unique transaction IDs and check if an operation was already performed before executing it.

**Compensation Failures**: What happens when a compensation action fails? You can't just retry forever. Implement a dead letter queue for failed compensations and alert operations teams for manual intervention.

**Lost Messages**: In choreography-based sagas, events can be lost. Use message brokers with persistence and implement event sourcing to maintain a complete audit trail.

**Temporal Coupling**: Long-running sagas can span minutes or hours. Services might be deployed or restarted during execution. Persist saga state to durable storage and implement recovery mechanisms.

**Inconsistent Reads**: During saga execution, the system is temporarily inconsistent. Users might see an order as "pending" while payment is processing. Design UIs to handle these intermediate states gracefully.

---

## Best Practices

**Persist Saga State**: Store saga execution state in a database. This enables recovery after crashes and provides visibility into in-flight transactions.

**Implement Timeouts**: Every step should have a timeout. If a service doesn't respond, fail fast and trigger compensation rather than waiting indefinitely.

**Use Semantic Lock**: Instead of database locks, use semantic locks (like order status) to prevent concurrent modifications.

**Monitor Saga Metrics**: Track saga completion rates, compensation frequency, and execution duration. High compensation rates indicate systemic issues.

**Design Compensations Carefully**: Some actions can't be truly undone (like sending an email). Design compensations to achieve semantic rollback (send cancellation email) rather than perfect rollback.

**Version Your Sagas**: As your system evolves, saga definitions will change. Version them and handle in-flight sagas from older versions gracefully.

---

## FAQ

**Q: Should I use orchestration or choreography?**
A: Orchestration is simpler to understand and debug—there's a central coordinator that knows the entire workflow. Use it for complex sagas with many steps. Choreography is more decoupled and scalable but harder to monitor. Use it for simple, linear workflows where services already emit domain events.

**Q: How do I handle partial failures in compensations?**
A: Implement a retry mechanism with exponential backoff. After exhausting retries, log to a dead letter queue and alert operations. Some compensations may require manual intervention—design your system to make this visible and actionable.

**Q: Can I use sagas with synchronous HTTP calls?**
A: Yes, but it's not ideal. Synchronous calls create tight coupling and reduce availability. If one service is down, the entire saga fails. Prefer asynchronous messaging with a message broker for better resilience.

**Q: How do I test sagas?**
A: Unit test individual steps and compensations. Integration test the entire saga with mocked services. Use chaos engineering to test failure scenarios—randomly fail steps and verify compensations execute correctly.

**Q: What's the difference between saga and two-phase commit?**
A: Two-phase commit (2PC) provides ACID guarantees but requires all participants to be available and locks resources during coordination. Sagas sacrifice isolation for availability—they don't lock resources and use compensations instead of rollbacks.

**Q: How do I handle concurrent sagas modifying the same resource?**
A: Use optimistic locking with version numbers or timestamps. When a saga tries to modify a resource, check if the version matches. If not, the saga fails and compensates. Alternatively, use a distributed lock service like Redis or ZooKeeper.

**Q: Should I expose saga state to users?**
A: Yes, but carefully. Users should see meaningful status like "Processing payment" or "Preparing shipment" rather than technical details. Provide estimated completion times and clear error messages if the saga fails.

---

The Saga pattern isn't a silver bullet—it trades ACID guarantees for availability and scalability. But in distributed systems, this trade-off is often necessary. By carefully designing compensations, handling failures gracefully, and monitoring saga execution, you can build resilient systems that maintain eventual consistency across service boundaries.
