In a microservices architecture where each service owns its private database (e.g. Order Service on PostgreSQL, Payment Service on Stripe/Redis, Inventory Service on DynamoDB), Two-Phase Commit is an anti-pattern.
Holding database row locks across HTTP/gRPC network boundaries destroys availability, introduces severe latency coupling, and creates single points of failure.
To maintain eventual data consistency across distributed services without distributed locks, the industry standard is The Saga Pattern (Garcia-Molina & Salem, 1987).
1. Decomposing Distributed Transactions into Sagas
A Saga decomposes a distributed transaction into a sequence of local ACID transactions:
- Each transaction executes locally within a single service's database and commits immediately.
- For every transaction , there exists a corresponding Compensating Transaction that semantically undoes the changes made by :
2. Choreography vs Orchestration
There are two primary architectural styles for implementing Sagas:
Architectural Comparison:
| Characteristic | Event Choreography | Centralized Orchestration |
|---|---|---|
| Coupling | Loosely coupled (Pub/Sub message broker) | Tightly coordinated by Orchestrator |
| Observability | Difficult (Traces scattered across Kafka topics) | Trivial (Single state machine execution history) |
| Complexity | Risk of cyclic dependency storms | Easy to manage complex business logic & loops |
| Tooling | Kafka, RabbitMQ, AWS EventBridge | Temporal.io, AWS Step Functions, Cadence |
| Best For | Simple 2-3 step asynchronous pipelines | Enterprise multi-step workflows (5+ services) |
3. The Lack of ACID Isolation: Semantic Anomalies
Because each local transaction commits immediately, Sagas guarantee Atomicity, Consistency, and Durability, but SACRIFICE ISOLATION:
Other concurrent transactions can observe the intermediate, uncommitted states of an ongoing Saga (Dirty Reads).
The 3 Classic Saga Anomalies & Countermeasures:
4. Code Deep-Dive: Saga Orchestrator State Machine
5. Production Failure Postmortem: The Non-Idempotent Double-Refund Loop
Incident Overview:
In 2021, an on-demand delivery app lost \140,!000$ in 30 minutes due to a non-idempotent compensating refund transaction triggered during a network timeout storm.
What Happened:
- When a restaurant rejected an order, the Saga orchestrator triggered the compensation:
RefundPaymentService.refund(orderId, amount). - The HTTP request timed out after , but the payment processor had actually processed the refund.
- The orchestrator's retry policy immediately re-attempted the compensation 3 times.
- Because the refund endpoint generated a new random
refund_idon each invocation rather than using theorderIdas an Idempotency Key, the customer was refunded 4 times for a single canceled order.
Key Lesson:
- Compensating transactions MUST be 100% idempotent. Always pass a deterministic idempotency key (
idempotency_key = "compensate:" + saga_id + ":" + step_name) to prevent duplicate execution during retries.
6. Landmark Capstone #5: Build a Distributed Saga Orchestration Engine ⚔️
Put your distributed transactions skills to the test by building an industrial-strength Saga Orchestrator:
👉 Launch Capstone: Build a Saga Orchestrator Engine
- Supported Languages: TypeScript & Python 3
- Challenge Focus:
- Implement sequential forward step execution.
- Track dynamic compensation stacks with LIFO rollback execution.
- Enforce idempotency on retry attempts.
- Handle partial step failures and preserve execution logs.