Two-Phase Commit and Why Almost Nobody Uses It
It gives you atomicity across systems and takes your availability in exchange. Why the blocking case is fatal, and what people use instead.
The problem is real and common: two systems must both change, or neither. Debit one account and credit another, across two databases. Reserve inventory and take payment, across two services.
For a separate people-operations perspective, this operational guide covers time rounding in payroll.
Two-phase commit solves it, correctly, and the cost is high enough that most systems choose to live with the problem instead.
The protocol
A coordinator manages participants.
Phase one — prepare. The coordinator asks each participant whether it can commit. Each does everything short of committing: validates, acquires locks, writes its intent durably. Then it answers yes or no.
A yes is a promise. The participant is now committed to being able to commit, whatever happens next. It cannot change its mind, and it holds its locks until told what to do.
Phase two — commit or abort. If all said yes, the coordinator tells everyone to commit. If any said no, everyone aborts.
The correctness argument is sound: nobody commits until everyone has promised they can.
The failure that makes it unusable
The coordinator crashes after collecting the yes votes and before sending the decision.
Every participant is now prepared. They have promised to commit and they hold their locks. They cannot commit — the decision may have been abort. They cannot abort — the decision may have been commit. They cannot ask each other, because none of them knows the decision either.
They block. Indefinitely. Holding locks.
This is not an implementation flaw. It is inherent: with a possibility of failure, no protocol can guarantee both atomicity and non-blocking termination.
The practical consequences:
The coordinator is a single point of failure whose unavailability is not degraded service but a stalled system.
Locks are held for the duration of the protocol, which spans network round trips to every participant. Under contention this destroys throughput.
Recovery requires human intervention in the general case. Someone must determine what happened and force the transaction one way, and getting it wrong corrupts data.
Availability multiplies. All participants must be up. Four participants at 99.9% each give the transaction under 99.6%.
Where it is still used
Not never — the cases where the trade is right.
Within one database across shards, where the coordinator is part of the same system, failure domains are shared, and recovery is automatic.
Between a database and a message broker that support a shared transaction manager, in environments built around this — traditional enterprise middleware does this and it works.
Low-volume, high-value operations where blocking occasionally is acceptable and inconsistency is not.
What has changed is that consensus protocols — Raft, Paxos — solve a related problem without the blocking case, by replacing the single coordinator with a quorum. They do not directly replace cross-system atomicity, and they are why modern distributed databases can offer atomic commits across shards without the classic failure mode.
What people use instead
Saga
Break the operation into local transactions, each with a compensating action. If a later step fails, run the compensations for the earlier ones.
There is no isolation. Intermediate states are visible. Between reserving inventory and taking payment, the inventory is reserved and unpaid, and other transactions see that.
Compensation is not rollback. You cannot un-send an email; you send an apology. You cannot un-charge a card; you issue a refund, which is a new transaction with its own record. Designing the compensating action is the real work, and for some operations there isn't one.
Compensations must be idempotent and must not fail, or you need compensation for the compensation. In practice the failure path terminates in an alert and a human.
Sagas are the common answer, and they trade correctness guarantees for availability quite explicitly.
Outbox
Where one side is your database and the other is a message or an external call.
Write the business change and a row describing the intended external effect in the same local transaction. A separate process reads the outbox and performs the effect, retrying until it succeeds.
One transaction, so atomicity holds, and the external effect happens at least once. The receiver must be idempotent. See every request will be retried.
This is the right answer far more often than a saga, because most cross-system operations are one local change plus one external effect rather than two peers.
Avoid the problem
The cheapest option and the one worth trying first.
Keep data that must change atomically in one place. Two services needing atomic updates across their databases is frequently a sign the service boundary is in the wrong place — the boundary should follow the transaction, not cut across it.
Make it eventually consistent and say so. Many operations do not need to be atomic; they need to converge. An order that is created and then confirmed a second later is usually acceptable, and designing for it is far cheaper than coordinating.
Reorder so the risky step is last. If step B can fail and step A cannot be undone, do B first.
Choosing
Can the operations live in one database? Do that. Everything else is more expensive.
Is it one local change plus one external effect? Outbox.
Is it a genuine multi-party workflow with meaningful intermediate states? Saga, with compensations designed explicitly and the visible intermediate states acknowledged in the interface.
Is it low-volume, high-value, with participants that support a shared transaction manager? Two-phase commit is defensible.
Is it high-volume across independent services? Two-phase commit is not the answer, and the question is usually whether atomicity is genuinely required or whether convergence would do.
The summary
Two-phase commit is correct and it blocks, and the blocking case takes locks with it.
The coordinator is a single point of failure whose loss stalls rather than degrades.
Sagas trade isolation for availability and make you design compensations, which is harder than it sounds and impossible for some actions.
The outbox covers the common case — one local change, one external effect — and it is the answer most often.
And the best move is usually to arrange not to need it, by putting data that changes together in one place.