Adetayo Akinsanya unkletayo.dev

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

  1. Phase 1 (Prepare / Voting Phase):
    • The Coordinator sends a Prepare message to all participant nodes.
    • Each participant opens a local transaction, writes undo/redo logs, locks required database rows, and votes VOTE_COMMIT (Yes) or VOTE_ABORT (No).
  2. Phase 2 (Commit / Rollback Phase):
    • If ALL participants vote VOTE_COMMIT, the Coordinator writes a Global Commit log entry to disk and sends Commit to all nodes. Nodes commit local transactions and release row locks.
    • If ANY participant votes VOTE_ABORT (or times out), the Coordinator sends Global Abort. All nodes roll back local transactions and release row locks.

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 (CiC_i)

If a local transaction TiT_i fails (e.g., T3T_3 Payment Fails), the Saga executes a series of Compensating Transactions (C2,C1C_2, C_1) 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

DimensionTwo-Phase Commit (2PC)Saga Pattern
Consistency ModelImmediate / Strong Consistency (ACID).Eventual Consistency (BASE).
Database LocksSynchronous row locks held across nodes.Zero cross-service locks.
Isolation LevelHigh (Prevents Dirty Reads).Low (Lack of Isolation; requires counter-measures).
Throughput & ScaleLow throughput (100\sim 100 tx/sec).High throughput (100,000+100,000+ tx/sec).
Recovery MechanismCoordinator Global Abort.Compensating Transactions (CiC_i).

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

  1. Burrows, M. (2006). The Chubby lock service for loosely-coupled distributed systems. Proceedings of OSDI ‘06, 335–350.
  2. Kleppmann, M. (2016). How to do distributed locking (Redlock critique). Martin Kleppmann’s Blog.
  3. Sanfilippo, S. (2016). Is Redlock safe? (Redis Creator Response). Antirez’s Blog.

Up Next in Series →

Part 8: Distributed Consensus Protocols: Paxos vs Raft Leader Election & Log Replication

Continue to Part 8 →