Two-Phase Commit (2PC)
debt(d7/e9/b7/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints note automated detection is 'no', and tools listed (phpstan, datadog) can at best flag XA transaction patterns or observe blocking in production — neither catches the coordinator failure risk or bottleneck at design time. The problem is architectural and typically only surfaces under failure conditions in production.
Closest to 'architectural rework' (e9). The quick_fix explicitly states to avoid 2PC entirely and replace with the Saga pattern with compensating transactions. This is not a line-level fix — it requires redesigning the distributed transaction model, implementing compensating transactions across services, potentially introducing an outbox pattern, and rethinking failure handling. This is a full architectural rework.
Closest to 'strong gravitational pull' (b7). 2PC applies across web, cli, and queue-worker contexts and shapes every distributed operation touching multiple databases or services. The coordinator becomes a single point of failure and bottleneck that every future change must account for. The locking requirement during the protocol constrains throughput and service design across the system, though it stops short of b9 since it applies only where distributed transactions are used.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field directly states the trap: developers believe 2PC solves distributed transactions reliably, but it is actually blocking — coordinator failure leaves participants stuck indefinitely. This contradicts the expectation of resilience that motivates using distributed transactions in the first place. The common mistakes reinforce that failure paths are rarely tested and the coordinator bottleneck is routinely underestimated.
Also Known As
TL;DR
Explanation
2PC is a consensus protocol: Phase 1 (Prepare) — a coordinator asks all participants to lock resources and vote commit/abort. Phase 2 (Commit/Rollback) — if all voted commit, the coordinator instructs commit; any abort triggers a full rollback. 2PC guarantees ACID atomicity across multiple databases but has significant costs: blocking (if the coordinator crashes after prepare, participants remain locked indefinitely), two network round trips per transaction, and reduced throughput. Most modern distributed systems prefer the Saga pattern for long-lived business transactions and accept eventual consistency rather than paying 2PC's availability and latency costs.
Diagram
sequenceDiagram
participant COORD as Coordinator
participant P1 as Participant 1 Orders DB
participant P2 as Participant 2 Payments DB
Note over COORD: Phase 1 - Prepare
COORD->>P1: PREPARE
COORD->>P2: PREPARE
P1-->>COORD: VOTE YES
P2-->>COORD: VOTE YES
Note over COORD: Phase 2 - Commit
COORD->>P1: COMMIT
COORD->>P2: COMMIT
P1-->>COORD: ACK
P2-->>COORD: ACK
Common Misconception
Why It Matters
Common Mistakes
- Using 2PC for high-throughput operations — the coordinator is a bottleneck and failure point.
- Not handling coordinator failure — if the coordinator crashes between prepare and commit, participants are blocked.
- Using 2PC when saga or outbox patterns would provide better availability with eventual consistency.
- Not testing the failure path — 2PC rollback code is rarely exercised and often broken.
Code Examples
// Distributed transaction without 2PC — inconsistent state on failure:
function transferFunds(): void {
$bankA->debit($amount); // Succeeds
$bankB->credit($amount); // Network failure — money lost!
}
// 2PC prepare phase: both banks lock funds
// 2PC commit phase: both banks execute if both prepared successfully
// Rollback phase: both debit/credit reversed if either failed
// 2PC: coordinator asks all participants to PREPARE, then COMMIT
// Ensures atomicity across distributed resources
// PHP example: transfer across two databases
function transfer(PDO $dbA, PDO $dbB, int $amount): void {
// Phase 1: PREPARE
$dbA->beginTransaction();
$dbB->beginTransaction();
try {
$dbA->exec("UPDATE accounts SET balance = balance - $amount WHERE id = 1");
$dbB->exec("UPDATE accounts SET balance = balance + $amount WHERE id = 2");
// Phase 2: COMMIT both
$dbA->commit();
$dbB->commit();
} catch (\Throwable $e) {
$dbA->rollBack();
$dbB->rollBack();
throw $e;
}
}
// 2PC is blocking — prefer Saga pattern for long-running distributed transactions