Distributed Transactions: Two-Phase Commit (2PC) vs The Saga Pattern
Understanding 2PC Prepare/Commit phases, coordinator single points of failure, Saga Choreography vs Orchestration, and compensating transactions
Part 7 in Series — Catch up on the previous article: Gossip Protocols & Cluster Membership: How Decentralized Nodes Maintain Topology (Part 6) before diving into this post.
Why You Need This in Real Life
In a monolithic architecture, placing an order, deducting inventory, charging a credit card, and updating shipping records happens inside a single ACID database transaction. If any SQL command fails, the database executes ROLLBACK, restoring data to a clean state.
In a microservice architecture, these four actions are managed by four independent microservices, each with its own database: OrderService, InventoryService, PaymentService, and ShippingService.
A customer submits an order. OrderService writes a record. InventoryService decrements stock. PaymentService attempts to charge the card, but the charge fails due to insufficient funds.
Without a distributed transaction protocol, OrderService and InventoryService have already committed their database writes! Stock is reserved for a customer who never paid, leaving inventory data corrupt.
To maintain cross-service consistency across independent databases, you must choose between Two-Phase Commit (2PC) and The Saga Pattern.
Part 1: Two-Phase Commit (2PC) Protocol
Two-Phase Commit (2PC) is a synchronous protocol that extends ACID guarantees across multiple database nodes using a central Transaction Coordinator.
Phase 1: Prepare Phase Phase 2: Commit Phase
Coordinator Participant A Participant B Coordinator Participant A Participant B
| | | | | |
|-- 1. Prepare ------>| | |-- 4. Global Commit->| |
|-- 1. Prepare ---------------------->| |-- 4. Global Commit------------------>|
| | | | | |
|<-- 2. VOTE_COMMIT --| | |<-- 5. ACK ----------| |
|<-- 3. VOTE_COMMIT ------------------| |<-- 5. ACK --------------------------|
The Two Phases Explained
- Phase 1 (Prepare / Voting Phase):
- The Coordinator sends a
Preparemessage to all participant nodes. - Each participant opens a local transaction, writes undo/redo logs, locks required database rows, and votes
VOTE_COMMIT(Yes) orVOTE_ABORT(No).
- The Coordinator sends a
- Phase 2 (Commit / Rollback Phase):
- If ALL participants vote
VOTE_COMMIT, the Coordinator writes aGlobal Commitlog entry to disk and sendsCommitto all nodes. Nodes commit local transactions and release row locks. - If ANY participant votes
VOTE_ABORT(or times out), the Coordinator sendsGlobal Abort. All nodes roll back local transactions and release row locks.
- If ALL participants vote
Why 2PC Fails at Scale: The Blocking Coordinator Trap
2PC provides strong ACID consistency, but introduces severe operational drawbacks in high-throughput microservices:
- Blocking Locks: Rows remain locked from Phase 1 until Phase 2 completes. If the Coordinator crashes mid-transaction after Phase 1, all participant databases remain locked indefinitely, starving incoming HTTP request threads.
- Latency Multiplier: Network round-trips for 2PC increase transaction latency by 10x to 50x.
- Single Point of Failure (SPOF): Coordinator crash leaves participants in an indeterminate blocking state.
Part 2: The Saga Pattern
To eliminate blocking database locks in microservices, modern architectures use The Saga Pattern.
A Saga is a sequence of local transactions. Each local transaction updates data within a single microservice database and publishes an event or message to trigger the next local transaction in another service.
Happy Path Saga Flow:
[ T1: Create Order ] ---> [ T2: Reserve Stock ] ---> [ T3: Charge Card ] ---> [ T4: Create Shipment ]
(Order Service DB) (Inventory DB) (Payment DB) (Shipping DB)
Handling Failures: Compensating Transactions ()
If a local transaction fails (e.g., Payment Fails), the Saga executes a series of Compensating Transactions () in reverse order to undo the changes made by previous local transactions:
Failure Rollback Flow:
[ T1: Create Order ] ---> [ T2: Reserve Stock ] ---> [ T3: Payment FAILS! ]
| |
v v
[ C1: Cancel Order ] <--- [ C2: Un-reserve Stock ] (Compensating Rollback Flow)
A compensating transaction does NOT perform a database rollback. It executes a forward undo action (e.g., issuing a refund or updating order status to CANCELLED).
Part 3: Saga Implementations: Choreography vs Orchestration
1. Choreography (Event-Driven Decentralized)
Microservices listen to domain events published via a message broker (e.g., Apache Kafka) and execute local transactions autonomously:
OrderService --------> Event: OrderCreated --------> InventoryService
|
PaymentService <------- Event: StockReserved <-------------+
- Pros: Simple, highly decoupled, no central coordinator.
- Cons: Difficult to trace complex flows; danger of cyclic event dependencies.
2. Orchestration (Centralized Workflow State Machine)
A dedicated Saga Orchestrator service manages the workflow state machine, invoking microservices via command messages:
+-----------------------+
| Saga Orchestrator |
+-----------+-----------+
|
+-----------------------+-----------------------+
| | |
v v v
[ OrderService ] [ InventoryService ] [ PaymentService ]
- Pros: Centralized visibility, easy to audit, prevents cyclic dependencies.
- Cons: Orchestrator service can become complex if business logic leaks into it.
Part 4: 2PC vs Saga Comparison Matrix
| Dimension | Two-Phase Commit (2PC) | Saga Pattern |
|---|---|---|
| Consistency Model | Immediate / Strong Consistency (ACID). | Eventual Consistency (BASE). |
| Database Locks | Synchronous row locks held across nodes. | Zero cross-service locks. |
| Isolation Level | High (Prevents Dirty Reads). | Low (Lack of Isolation; requires counter-measures). |
| Throughput & Scale | Low throughput ( tx/sec). | High throughput ( tx/sec). |
| Recovery Mechanism | Coordinator Global Abort. | Compensating Transactions (). |
Part 5: Java Saga Orchestrator State Machine
package com.example.saga;
import java.util.*;
public class OrderSagaOrchestrator {
public enum SagaState { CREATED, STOCK_RESERVED, PAID, FAILED, CANCELLED }
public record SagaContext(String orderId, String userId, double amount, SagaState state) {}
public boolean executeOrderSaga(String orderId, String userId, double amount) {
SagaContext context = new SagaContext(orderId, userId, amount, SagaState.CREATED);
System.out.println("[SAGA ORCHESTRATOR] Starting Saga for Order: " + orderId);
// Step 1: Create Local Pending Order
boolean orderCreated = createPendingOrder(context);
if (!orderCreated) {
System.err.println("[SAGA FAILED] Step 1: Order creation failed");
return false;
}
// Step 2: Reserve Stock
boolean stockReserved = reserveStock(context);
if (!stockReserved) {
System.err.println("[SAGA FAILED] Step 2: Stock reservation failed. Executing Compensations...");
compensateCreatePendingOrder(context);
return false;
}
// Step 3: Charge Payment
boolean paymentSuccess = processPayment(context);
if (!paymentSuccess) {
System.err.println("[SAGA FAILED] Step 3: Payment failed. Executing Compensations in Reverse...");
compensateReserveStock(context); // C2
compensateCreatePendingOrder(context); // C1
return false;
}
System.out.println("[SAGA SUCCESS] Order " + orderId + " completed successfully!");
return true;
}
private boolean createPendingOrder(SagaContext ctx) {
System.out.println(" -> [T1] OrderService: Pending order created.");
return true;
}
private boolean reserveStock(SagaContext ctx) {
System.out.println(" -> [T2] InventoryService: Stock reserved.");
return true;
}
private boolean processPayment(SagaContext ctx) {
System.out.println(" -> [T3] PaymentService: Charging card failed!");
return false; // Simulate payment failure
}
// Compensating Transactions (Undo Actions)
private void compensateReserveStock(SagaContext ctx) {
System.out.println(" <- [C2] InventoryService: Un-reserving stock.");
}
private void compensateCreatePendingOrder(SagaContext ctx) {
System.out.println(" <- [C1] OrderService: Updating order status to CANCELLED.");
}
}
Next Steps
Now that we understand distributed transactions, 2PC, and the Saga pattern, we will examine Consensus Protocols in Part 8: dissecting Paxos and Raft leader election algorithms.
References & Further Reading
- Burrows, M. (2006). The Chubby lock service for loosely-coupled distributed systems. Proceedings of OSDI ‘06, 335–350.
- Kleppmann, M. (2016). How to do distributed locking (Redlock critique). Martin Kleppmann’s Blog.
- Sanfilippo, S. (2016). Is Redlock safe? (Redis Creator Response). Antirez’s Blog.
Part 8: Distributed Consensus Protocols: Paxos vs Raft Leader Election & Log Replication
Continue to Part 8 →