How to Implement a Two-Phase Commit Mock in Python
Simulate a distributed two-phase commit with prepare, commit, and abort phases, including deterministic failure injection for testing.
Python code
70 linesimport random
from dataclasses import dataclass
from typing import Dict, List, Optional
@dataclass
class Transaction:
tx_id: int
data: Dict[str, str]
class TwoPhaseCommitMock:
"""Simple two-phase commit mock with prepare and commit phases."""
def __init__(self) -> None:
self.prepared: List[int] = []
self.committed: Dict[int, Transaction] = {}
self.fail_on_prepare: set[int] = set()
self.fail_on_commit: set[int] = set()
def prepare(self, tx: Transaction) -> bool:
# Simulate possible failure during prepare phase
if tx.tx_id in self.fail_on_prepare or random.random() < 0.1:
return False
self.prepared.append(tx.tx_id)
return True
def commit(self, tx: Transaction) -> bool:
if tx.tx_id not in self.prepared:
print(f"Transaction {tx.tx_id} not prepared, cannot commit")
return False
if tx.tx_id in self.fail_on_commit:
return False
self.committed[tx.tx_id] = tx
return True
def abort(self, tx: Transaction) -> None:
if tx.tx_id in self.prepared:
self.prepared.remove(tx.tx_id)
print(f"Transaction {tx.tx_id} aborted")
def run_transaction(self, tx: Transaction) -> bool:
"""Execute a full two-phase commit transaction."""
if not self.prepare(tx):
print(f"Prepare failed for transaction {tx.tx_id}, aborting")
self.abort(tx)
return False
if not self.commit(tx):
print(f"Commit failed for transaction {tx.tx_id}, aborting")
self.abort(tx)
return False
print(f"Transaction {tx.tx_id} committed successfully")
return True
if __name__ == "__main__":
# Deterministic mock for demonstration
mock = TwoPhaseCommitMock()
mock.fail_on_prepare = {2} # Always fail on transaction 2
mock.fail_on_commit = {3} # Always fail on commit for transaction 3
t1 = Transaction(1, {"account": "A", "amount": "100"})
t2 = Transaction(2, {"account": "B", "amount": "50"})
t3 = Transaction(3, {"account": "C", "amount": "75"})
for tx in [t1, t2, t3]:
mock.run_transaction(tx)
print(f"\nCommitted transactions: {sorted(mock.committed.keys())}")
print(f"Prepared (but maybe uncommitted): {mock.prepared}")
Output
Prepare failed for transaction 2, aborting
Transaction 2 aborted
Commit failed for transaction 3, aborting
Transaction 3 aborted
Transaction 1 committed successfully
Committed transactions: [1]
Prepared (but maybe uncommitted): []
How it works
The TwoPhaseCommitMock wraps the coordinator logic for a simplified 2PC: each transaction first goes through prepare, then commit, with abort as a fallback for either failure. fail_on_prepare and fail_on_commit sets let you inject deterministic failures, while random.random() < 0.1 adds stochastic failure for realistic testing. The run_transaction method orchestrates the whole flow, ensuring a transaction is never committed unless it was successfully prepared first. This mirrors the core guarantee of two-phase commit — atomicity across participants — without needing real network or database resources.
Common mistakes
- Forgetting to check membership in `prepared` before allowing a commit, breaking the prepare-then-commit invariant.
- Using mutable class-level attributes for state, causing cross-instance contamination when multiple mocks exist.
- Not providing a way to simulate failures, making it impossible to test abort and rollback logic.
- Ignoring stochatic failure in tests, leading to flaky assertions when randomness is not seeded.
Variations
- Use a state machine (e.g., `enum` with PREPARING, PREPARED, COMMITTED) to enforce phase transitions more strictly.
- Replace the dataclass with a generic `Transaction` protocol so it can wrap different payload types in real services.
Real-world use cases
- Unit-testing a transaction coordinator's retry and rollback logic without spinning up multiple database connections.
- Simulating network partitions or node crashes in a microservices integration test to verify the saga compensates correctly.
- Teaching or demoing the 2PC protocol in a classroom or onboarding session with deterministic, reproducible failures.
Sponsored
More from Microservices patterns
- BFF aggregation pattern: combine multiple service responses in Python easy
- Backward Compatible Schema Evolution in Python medium
- Bulkhead Thread Pool per Service Mock in Python medium
- CQRS with Separate Read and Write Repositories in Python medium
- Cache-Aside Pattern in Python: Per-Service Mock easy
- Consumer Driven Contract Pact Mock in Python medium
Keep learning
Related tutorials and quizzes for this topic.