Table of Contents
In distributed database architecture (PostgreSQL Shards, Oracle RAC, CockroachDB, Google Spanner), executing multi-node ACID transactions requires Atomic Commitment Protocols.
When a transaction spans multiple database shards, all participating nodes (Cohorts) must agree to either Commit the changes permanently or Abort them entirely.
If a single shard commits while another aborts, the database enters an unrecoverable state of data corruption.
For decades, distributed systems relied on the Two-Phase Commit (2PC) protocol.
However, classic 2PC suffers from a severe Blocking Protocol Flaw: if the Transaction Coordinator crashes after cohorts vote YES, participating database nodes are left holding locks indefinitely.
While Three-Phase Commit (3PC) attempted to fix blocking, it fails under real-world network partitions. Modern cloud databases solve this by pairing 2PC with Paxos or Raft Consensus Replicated Transaction Logs.
This article details 2PC Prepare/Commit phases, Coordinator blocking vulnerabilities, 3PC Pre-Commit states, network partition failures, and Paxos/Raft-backed transaction log replication.
Distributed Atomic Commit Architecture
How classic Two-Phase Commit (2PC) operates, its blocking flaw, and how Raft consensus groups make 2PC fault-tolerant:
Core Atomic Commitment Mechanics
- Classic Two-Phase Commit (2PC):
- Phase 1 (Prepare / Voting):
- Coordinator logs
START_2PCto its local Write-Ahead Log (WAL) and sendsPREPAREmessages to all Cohorts. - Each Cohort executes the transaction locally up to the point of commit, acquires row locks, logs changes to its local WAL, and votes
VOTE_COMMIT(orVOTE_ABORT).
- Coordinator logs
- Phase 2 (Commit / Rollback):
- If all Cohorts vote
VOTE_COMMIT, the Coordinator logsGLOBAL_COMMITand sendsCOMMITcommands to all Cohorts. - If any Cohort votes
VOTE_ABORT(or times out), the Coordinator logsGLOBAL_ABORTand orders all Cohorts to rollback.
- If all Cohorts vote
- Phase 1 (Prepare / Voting):
- The 2PC Blocking Protocol Vulnerability:
- The Problem: 2PC is a blocking protocol. If the Coordinator crashes after Cohorts send
VOTE_COMMITbut before broadcastingGLOBAL_COMMIT, Cohorts have no way of knowing whether the transaction committed or aborted. - Impact: Cohorts must hold local row locks indefinitely to prevent dirty reads, halting subsequent queries on those table rows!
- The Problem: 2PC is a blocking protocol. If the Coordinator crashes after Cohorts send
- Three-Phase Commit (3PC) & Network Partitions:
- 3PC attempts to solve blocking by inserting an intermediate
PRE-COMMITphase between Voting and Commit. - Why 3PC Fails: 3PC assumes an asynchronous "fail-stop" network model. In real networks subject to Network Partitions (Split-Brain), 3PC can cause different partitions to commit and abort simultaneously, violating atomicity!
- 3PC attempts to solve blocking by inserting an intermediate
- Modern Solution: Raft / Paxos-Backed Replicated Transaction Logs:
- Modern distributed SQL engines (CockroachDB, Spanner, YugabyteDB) do not run 2PC with single-point-of-failure Coordinator nodes.
- Instead, the Transaction Coordinator itself is a Raft / Paxos Consensus Group.
- If the active Raft Leader Coordinator crashes, Raft elects a new Leader in milliseconds. The new Leader inspects the replicated Raft log and completes the 2PC commit phase without blocking any database shards!
Python Implementation: 2PC Coordinator & Raft Consensus Replicator
Here is a production-grade Python implementation of a 2PC Transaction Coordinator integrated with a Raft-replicated state log:
import time
from typing import Dict, List, Optional
from pydantic import BaseModel
class CohortVote(BaseModel):
cohort_id: str
vote_commit: bool
locked_keys: List[str]
class TwoPhaseCommitEngine:
"""
Simulates Two-Phase Commit (2PC) with Raft Consensus Replicated Transaction Log.
"""
def __init__(self, cohort_ids: List[str]):
self.cohort_ids = cohort_ids
self.cohort_locks: Dict[str, Set[str]] = {cid: set() for cid in cohort_ids}
self.replicated_txn_log: List[str] = []
def execute_2pc_transaction(self, txn_id: str, keys: List[str], should_fail_cohort: Optional[str] = None) -> bool:
print(f"\nš [2PC Transaction Started] TxnID '{txn_id}' targeting keys {keys}...")
# --- PHASE 1: PREPARE / VOTING PHASE ---
print(" š [Phase 1: PREPARE] Coordinator sending PREPARE to all Cohorts...")
votes: List[CohortVote] = []
for cid in self.cohort_ids:
if cid == should_fail_cohort:
print(f" ⢠Cohort '{cid}' VOTED ABORT ā (Local Constraint Failure)")
votes.append(CohortVote(cohort_id=cid, vote_commit=False, locked_keys=[]))
else:
self.cohort_locks[cid].update(keys)
print(f" ⢠Cohort '{cid}' VOTED COMMIT ā
(Acquired Locks on {keys})")
votes.append(CohortVote(cohort_id=cid, vote_commit=True, locked_keys=keys))
# Check Voting Consensus
all_yes = all(v.vote_commit for v in votes)
# --- PHASE 2: COMMIT / ABORT PHASE ---
if all_yes:
print(" š [Phase 2: GLOBAL_COMMIT] All Cohorts voted YES. Replicating COMMIT to Raft Log...")
self.replicated_txn_log.append(f"COMMIT:{txn_id}")
for cid in self.cohort_ids:
self.cohort_locks[cid].difference_update(keys)
print(f" ⢠Cohort '{cid}' COMMITTED changes and released locks.")
print(f" š [Txn Success] TxnID '{txn_id}' committed across all shards!")
return True
else:
print(" š„ [Phase 2: GLOBAL_ABORT] One or more Cohorts voted NO. Replicating ABORT to Raft Log...")
self.replicated_txn_log.append(f"ABORT:{txn_id}")
for cid in self.cohort_ids:
self.cohort_locks[cid].difference_update(keys)
print(f" ⢠Cohort '{cid}' ROLLED BACK changes and released locks.")
print(f" š“ [Txn Aborted] TxnID '{txn_id}' safely rolled back.")
return False
# Demonstration Execution
if __name__ == "__main__":
engine = TwoPhaseCommitEngine(cohort_ids=["shard_us_east", "shard_us_west", "shard_eu_central"])
print("š Demonstrating 2PC Atomic Commit & Consensus Replicated Logs...")
print("=" * 75)
# 1. Successful Distributed Transaction Across 3 Shards
engine.execute_2pc_transaction(txn_id="txn_1001", keys=["user_account_42", "ledger_balance_42"])
# 2. Failed Distributed Transaction (Cohort 3 Votes NO)
engine.execute_2pc_transaction(txn_id="txn_1002", keys=["user_account_99"], should_fail_cohort="shard_eu_central")
Distributed Transaction Gotchas & Best Practices
When engineering multi-shard database architectures:
Use Raft/Paxos Replicated Coordinators to Eliminate 2PC Blocking: Never deploy single-node 2PC coordinators in production. Wrap coordinator state inside a multi-node Paxos/Raft consensus group so leader failure triggers automatic, non-blocking failover.
Minimize Cross-Shard Multi-Partition Transactions: 2PC requires network round-trips to every participating shard. Design database primary keys (e.g. hash partitioning by tenant_id) so $95%+$ of transactions are fulfilled within a single shard node!
Real-World Enterprise Impact
Consensus-backed atomic commit protocols (in Google Spanner, CockroachDB, and YugabyteDB) report:
- Zero 2PC Lock Deadlocks on Coordinator Crashes: Replicating transaction coordinator state via Raft consensus allows instant leader failover without stalling database locks.
- $100%$ Multi-Shard ACID Integrity: Guarantees zero partial commit corruptions across globally distributed database clusters.

Discussion & Comments