Distributed Transactions: Demystifying the Two-Phase Commit Protocol

Arpit Bhayani

Arpit Bhayani

Mar 28, 2022 • 5 min read

Play

Distributed Transactions: Demystifying the Two-Phase Commit Protocol

Distributed transactions are among the most challenging concepts in modern software architecture. When an application transitions from a monolithic database to a microservice architecture, simple ACID guarantees provided out of the box by relational databases cease to exist. A single logical business transaction now spans multiple network boundaries, disparate databases, and independent services.

The Two-Phase Commit (2PC) protocol is a foundational consensus-like mechanism designed to achieve cross-service Atomicity (all operations succeed or none do) and Isolation in distributed systems.


The Problem: The 10-Minute Food Delivery Constraint

Consider a real-world hyper-local 10-minute food delivery guarantee (such as Zomato Instant):

  1. Dark Store Warehousing: Demand is predicted per locality; food is purchased in bulk and stocked at localized micro-warehouses (“dark stores”).
  2. Execution Window: Once an order is placed, the store has 2 minutes to warm and pack the meal, and a delivery partner has 8 minutes to pick up and deliver the food to the customer.

The Engineering Invariant

To guarantee a 10-minute delivery, an order cannot be placed unless two mutually dependent conditions are simultaneously true:

  • The exact food item is physically available in the designated local store.
  • A delivery partner is physically stationed nearby and available to take the run.
                    ┌────────────────┐
                    │  User Request  │
                    └───────┬────────┘


                   ┌──────────────────┐
                   │  Order Service   │ (Transaction Coordinator)
                   └───┬──────────┬───┘
           Reserve/    │          │    Reserve/
           Book Food   │          │    Book Rider
                       ▼          ▼
    ┌────────────────────┐      ┌──────────────────────┐
    │   Store Service    │      │   Delivery Service   │
    │ (Food Inventory)   │      │  (Fleet Management)  │
    └────────────────────┘      └──────────────────────┘

Why Naive REST Calls Fail (The Partial Failure Problem)

Suppose the Order Service sequentially executes simple HTTP REST calls without transactional coordination:

# NAIVE (BUGGY) IMPLEMENTATION
def place_order(user_id, item_id):
    # Step 1: Assign food
    store_service.assign_food(item_id)
    
    # Step 2: Assign driver
    delivery_service.assign_driver(user_id)
    
    # Step 3: Record order
    order_db.save(user_id, item_id)

If Step 1 succeeds but Step 2 fails (e.g., no drivers are available, or the delivery service times out):

  • The store receives the command, begins warming up the food, and packs it.
  • No delivery driver ever arrives.
  • Consequences: Food hygiene degrades due to reheating, physical inventory is wasted, and the business incurs a direct financial loss.

Conversely, if Step 2 succeeds and Step 1 fails:

  • A delivery agent is summoned to the dark store.
  • The requested food item is out of stock.
  • Consequences: Driver idle time, lost wages, and a severely degraded partner experience.

Because network partitions and hardware failures are inevitable, you must ensure all-or-nothing atomicity across separate service boundaries.


The Two-Phase Commit (2PC) Protocol

Two-Phase Commit solves this dilemma by dividing the execution into two distinct stages: the Prepare Phase (Voting/Reservation) and the Commit Phase (Execution/Assignment). In this topology, the Order Service acts as the Coordinator, while the Store Service and Delivery Service act as Participants (or Cohorts).

sequenceDiagram
    autonumber
    participant C as Order Service (Coordinator)
    participant S as Store Service (Participant 1)
    participant D as Delivery Service (Participant 2)
    
    Note over C,D: PHASE 1: PREPARE (Reservation Phase)
    C->>S: Prepare: Reserve Food (Item Lock + TTL)
    S-->>C: Vote YES (Reserved)
    C->>D: Prepare: Reserve Driver (Driver Lock + TTL)
    D-->>C: Vote YES (Reserved)
    
    Note over C,D: PHASE 2: COMMIT (Execution Phase)
    C->>S: Commit: Book Reserved Food
    S-->>C: Acknowledged (Cooking/Packing Initiated)
    C->>D: Commit: Assign Reserved Driver
    D-->>C: Acknowledged (Dispatch Dispatched)
    
    Note over C: Finalize Order in Orders DB

Phase 1: Prepare (Reservation Phase)

Instead of immediately triggering downstream side effects (such as instructing staff to microwave food or dispatching an agent), the coordinator first asks participants if they can guarantee the necessary resources.

  1. Reserve Food: The coordinator calls store_service.reserve_food(item_id, order_id). The store service acquires a row-level or semantic lock on that specific unit in its database, marking it as RESERVED. The kitchen staff is not notified.
  2. Reserve Driver: The coordinator calls delivery_service.reserve_driver(order_id). The delivery service locks an available partner in its database, marking them as RESERVED. The driver is not notified.

What is a Reservation?

A reservation guarantees exclusivity. It ensures that no concurrent transaction can claim that specific burger or driver while this transaction is under consideration.


Phase 2: Commit (Assignment Phase)

Once the coordinator successfully receives a YES vote from every participant:

  1. Book Food: The coordinator invokes store_service.book_food(reservation_id). The food state changes from RESERVED to COMMITTED. The dark store screen updates: staff begins heating and packaging the order.
  2. Book Driver: The coordinator invokes delivery_service.book_driver(reservation_id). The partner is officially dispatched to the dark store.
  3. Confirm to Customer: Once both commit acknowledgments return, the coordinator commits the order to the Orders DB and notifies the client that their 10-minute delivery has begun.

Because the items were already reserved during Phase 1, contention is zero during Phase 2. Barring transient network blips, commit calls are guaranteed to succeed.


Failure Handling & Fault Tolerance

Distributed systems fail in unpredictable ways. 2PC accounts for these failure modes via deterministic rollbacks and time-based resource leases.

1. Failure During the Prepare Phase (Rollback)

If any participant votes NO (e.g., food is out of stock or no drivers are nearby) or fails to respond within a timeout window:

sequenceDiagram
    autonumber
    participant C as Order Service
    participant S as Store Service
    participant D as Delivery Service
    
    C->>S: Prepare: Reserve Food
    S-->>C: OK (Food Reserved)
    C->>D: Prepare: Reserve Driver
    D-->>C: ERROR / Timeout (No Drivers)
    
    Note over C: Transaction Aborted
    C->>S: Abort: Cancel Reservation (Release Lock)
    S-->>C: Acknowledged (Lock Released)
    Note over C: Inform User:
Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses