Chuyển đến nội dung chính

Lesson 8: Saga Pattern & Distributed Transactions

Why doesn't ACID work in Microservices. Saga Pattern: Choreography vs Orchestration. Compensating transactions, idempotency, and error handling. Practical example: Order → Payment → Inventory → Shipping workflow.

🏗️ Architecture — Lesson 8 Lesson 8: Saga Pattern & Distributed Transactions

Microservices & Micro Frontend system design — From basics to Production

Part 3: Data Architecture in Microservices

xdev.asia

Introduction

In Monolith, transactions are simple: BEGIN → INSERT order → UPDATE inventory → COMMIT. In Microservices, each service has its own DB → cannot use distributed ACID transactions (2PC is too slow, fragile). Saga Pattern is the standard solution.

Saga Pattern — Choreography and Orchestration for distributed transactions


1. Problem: Distributed Transactions

1.1 Why is 2PC (Two-Phase Commit) not suitable?

  • Blocking: all participants are locked until the coordinator decides
  • Single point of failure: coordinator crash → deadlock
  • Performance hit: latency increases significantly
  • No scale: add participant = add complexity exponential

1.2 Accept Eventual Consistency

In Microservices, we accept eventual consistency instead of strong consistency. The data will be consistent eventually, not immediately.


2. Saga Pattern

Saga = string of local transactions, each transaction updates 1 service. If a transaction fails, the compensating transactions undo previous changes.

2.1 Choreography-based Saga

Each service publish event → the next service listens and acts:

Order Service         Payment Service       Inventory Service
     │                      │                      │
     │──OrderCreated───────►│                      │
     │                      │──PaymentProcessed───►│
     │                      │                      │──InventoryReserved──►
     │                      │                      │
     │ (nếu Inventory fail)                        │
     │                      │◄──InventoryFailed────│
     │◄──PaymentRefunded────│                      │
     │──OrderCancelled      │                      │

Advantages: Simple, loosely coupled, no central coordinator Disadvantages: Hard to track overall flow, cyclic dependencies possible

2.2 Orchestration-based Saga

Saga Orchestrator coordinates the entire flow:

                    ┌─────────────────┐
                    │ Order Saga      │
                    │ Orchestrator    │
                    └──┬──┬──┬───────┘
                       │  │  │
            ┌──────────┘  │  └──────────┐
            ▼             ▼             ▼
      ┌──────────┐ ┌──────────┐ ┌──────────┐
      │ Payment  │ │Inventory │ │ Shipping │
      │ Service  │ │ Service  │ │ Service  │
      └──────────┘ └──────────┘ └──────────┘

Orchestrator:
1. Create Order (PENDING)
2. Command: ProcessPayment → Payment Service
3. If success: Command: ReserveInventory → Inventory Service
4. If success: Command: CreateShipment → Shipping Service
5. If any fail: Compensate previous steps in reverse

Advantages: Clear flow, easy to understand, centralized error handling Disadvantages: Orchestrator can become bottleneck, single point of failure

2.3 When to use what?

CriteriaChoreographyOrchestration
Complexity2-3 steps4+ steps
VisibilityDifficult to trackClear
CouplingLooseMedium (to orchestrator)
Error handlingDistributedCentralized

3. Compensating Transactions

Compensating transaction = "undo" for business transaction (not DB rollback):

Forward:                  Compensating:
CreateOrder         →     CancelOrder
ProcessPayment      →     RefundPayment
ReserveInventory    →     ReleaseInventory
CreateShipment      →     CancelShipment

Note: Compensating transaction must be idempotent — called multiple times with the same result.


4. Idempotency — Handling Duplicate Messages

In a distributed system, messages can be sent multiple times (at-least-once delivery). Service must handle duplicates:

// Idempotent payment processing
async function processPayment(command) {
  // Check idempotency key
  const existing = await db.findPayment(command.idempotencyKey);
  if (existing) return existing; // Already processed
  
  // Process payment
  const result = await stripe.charge(command.amount);
  await db.savePayment({
    idempotencyKey: command.idempotencyKey,
    ...result
  });
  return result;
}

5. Hands-on: E-Commerce Order Saga

Create Order Saga (Orchestration):

Step 1: Order Service   → CreateOrder(PENDING)
Step 2: Payment Service → ChargeCustomer(orderId, amount)
  ✅ → Step 3
  ❌ → Compensate: CancelOrder → DONE (Order: PAYMENT_FAILED)

Step 3: Inventory Service → ReserveItems(orderId, items)
  ✅ → Step 4
  ❌ → Compensate: RefundPayment → CancelOrder → DONE (Order: OUT_OF_STOCK)

Step 4: Order Service → ConfirmOrder(orderId)
  ✅ → DONE (Order: CONFIRMED)
  
Step 5 (async): Notification Service → SendConfirmationEmail

Summary

  • 2PC is not suitable for Microservices — too slow and fragile
  • Saga Pattern = sequence of local transactions + compensating transactions
  • Choreography: simple flows (2-3 steps), loose coupling
  • Orchestration: complex flows (4+ steps), clear visibility
  • Idempotency is required — always handle duplicate messages

Next article: Lesson 9: Event Sourcing & CQRS — When to need it, when not to?