Imagine an application moving ten credits from one account to another. When both accounts live in one database, a transaction can subtract ten from the first and add ten to the second together. If the transaction fails, neither change remains committed.

Now put the accounts in two separate databases. Calling Commit on the first connection and then the second leaves a gap: the first commit can succeed while the second database becomes unavailable. Two-phase commit is a protocol for coordinating that decision across participating databases, including when messages or processes fail.

Introduction

This article explains two-phase commit, commonly shortened to 2PC, through a small application with two databases. We will follow the successful path, examine the promises each database makes and walk through the failure that can leave work waiting for a decision.

The account-credit example is a teaching model, not a recommendation to split a simple balance transfer across databases. Keeping a transaction inside one database is often the most straightforward design. The example makes the coordination problem visible without requiring a large microservices architecture.

By the end, you should understand what prepared means, why a coordinator needs durable records, why a timeout cannot always mean rollback and which alternatives change the business contract instead of reproducing 2PC under another name.

Begin with the Single-Database Guarantee

Suppose account A contains 40 credits and account B contains 20. A successful transfer leaves A with 30 and B with 30. The combined total remains 60. A rejected transfer leaves the original balances unchanged.

Inside one transactional database, the application can validate available credit, update both accounts and commit one transaction. The database's concurrency controls and constraints must also prevent incompatible simultaneous transfers, but there is one local commit decision.

If the application crashes before committing, database recovery can discard its uncommitted changes. If commit succeeds and the response is lost, the application still needs an operation identifier to discover the result instead of blindly transferring again.

That final ambiguity exists even without distribution. Two-phase commit addresses agreement among transaction participants; it does not eliminate the possibility that a caller loses the response to a successfully completed operation.

Keeping these responsibilities separate is helpful. The database coordinates its transaction, the application records the business operation and the API makes retries refer to that same operation.

See Why Two Ordinary Commits Are Insufficient

Place account A in Database North and account B in Database South. The application opens a transaction in each, performs the debit and credit, then calls Commit on North followed by South.

North commits the debit. Before South receives its commit request, the application process crashes and South rolls back its ordinary open transaction. The durable balances are now 30 and 20: ten credits have disappeared from the example's ledger.

Reversing the commit order changes the failure, rather than removing it. If South commits first and North does not, the durable balances temporarily gain ten credits. Sending both commit requests concurrently also leaves outcomes that can differ.

A try/catch block cannot reliably undo a commit. The compensating update is another transaction that can fail, encounter concurrent changes or violate a business rule. A deliberate compensation workflow can be valid, but it provides a different contract from one coordinated atomic decision.

We need a point at which each database can promise that it will follow a later common decision, even after the original connection disappears.

Meet the Coordinator and Participants

The coordinator manages the overall transaction. It assigns an identity, tracks which databases are involved, collects their responses and records the final decision. A transaction manager is the software component that normally performs this role.

The participants are the databases or other transactional resources performing local parts of the work. North owns the debit; South owns the credit. Both must support the required preparation and recovery operations.

Application requests transfer T42
|
Coordinator
/ \
Database North Database South
debit 10 credit 10

The coordinator is more than an application object holding two connections. Its state must survive a crash, and its recovery process must be able to contact prepared participants later.

A normal HTTP service cannot become a participant just because it exposes an endpoint called Prepare. It would need a complete, durable contract for reserving its changes, reporting its vote and recovering the final outcome. Names alone do not provide the protocol.

Understand What Preparing Means

Before preparation, a participant is still performing ordinary transactional work. It can discover a constraint violation or insufficient credit and refuse to proceed. Nothing requires the overall transfer to commit at that stage.

When a participant successfully prepares, it makes a stronger promise: its local work is durably ready for the coordinator's later commit or rollback decision. It must retain enough information and required resources to honour that decision after recovery.

Prepared is not committed. Other transactions do not simply receive the prepared changes as successful business state. The participant may continue holding locks or retaining older data versions while the decision is unresolved.

PostgreSQL exposes this state through PREPARE TRANSACTION. Its documentation makes clear that the feature is intended for an external transaction manager and that prepared transactions retain resources until resolved.

Think of preparation as an irrevocable vote under the protocol, rather than a tentative “probably fine”. Once a participant has voted yes, it cannot independently change its mind simply because waiting has become inconvenient.

Follow the First Phase: Ask Whether Commit Is Possible

After both local changes are ready, the coordinator asks each participant to prepare transaction T42. North validates and durably prepares the debit. South does the same for the credit.

Each returns yes only after its preparation requirements have been satisfied. If South cannot prepare because a constraint fails or a required resource is unavailable, it returns no or fails to provide a successful vote.

Coordinator -> North: prepare T42
North -> Coordinator: yes

Coordinator -> South: prepare T42
South -> Coordinator: yes

The coordinator cannot decide commit merely because one database prepared successfully. Every required participant must be ready. Otherwise it must choose the abort path according to its protocol and recovery rules.

This phase can take longer than expected because preparation is durable work, not a lightweight health check. Network delays, storage writes and local transaction contention all contribute to the time that other participants may already be holding resources.

Follow the Second Phase: Record and Deliver One Decision

Once every participant has voted yes, the coordinator durably records the decision to commit T42. The ordering is essential: it must be able to recover that decision before telling any participant to commit.

It then sends the commit decision to North and South. Each commits its prepared transaction, releases the relevant resources and acknowledges completion. The coordinator keeps enough history to finish recovery if an acknowledgement is lost.

Coordinator: durably record COMMIT for T42
Coordinator -> North: commit prepared T42
Coordinator -> South: commit prepared T42
North and South: finish the recorded decision

If either participant voted no, the recorded decision is abort and prepared participants are instructed to roll back. Both outcomes are valid. The requirement is that the participants resolve the overall transaction consistently.

PostgreSQL's COMMIT PREPARED documentation shows the local completion operation. Issuing that command is only one small part of the overall protocol; the coordinator's durable decision and recovery responsibilities remain necessary.

Walk Through a Refused Transfer

Suppose North discovers that account A has only five available credits when the ten-credit transfer is validated. North cannot prepare the debit under the account rule, so the overall operation must abort.

South may already be prepared to credit ten. It waits for the coordinator to send the abort decision, then rolls back that prepared credit. It does not keep the credit merely because its own local update was valid.

The application returns a business rejection after establishing the outcome it needs to report. Its operation record can distinguish insufficient credit from a temporary infrastructure failure, helping the caller decide whether another attempt makes sense.

This illustrates an important separation: a yes vote says the local participant can commit, not that it should commit independently. South has no authority to turn its preparation into a successful transfer on its own.

In a well-designed implementation, the negative outcome is routine. The protocol should not require manual database cleanup every time an ordinary business validation fails.

Examine the Blocking Failure

Now both databases vote yes, but the coordinator becomes unreachable before North learns the final decision. North has a prepared debit and must decide what to do next.

It cannot safely commit because South may have failed to prepare and the coordinator may have decided abort. It cannot safely roll back because the coordinator may already have recorded commit and delivered that decision to South.

From North's limited view, both histories can look like silence. Waiting preserves agreement until the decision can be recovered, but resources remain tied up. This is the classic blocking problem associated with two-phase commit.

Jim Gray and Leslie Lamport's paper Consensus on Transaction Commit explains the relationship between transaction commit, failures and agreement protocols. For an application engineer, the practical consequence is that some failures turn into unresolved prepared work rather than an immediate clean error.

The rest of the database may continue operating. Blocking can affect only transactions needing the held rows or other conflicting resources. However, a frequently used account or inventory record can make a narrow unresolved transaction cause broad application delays.

Understand Why a Timeout Is Not a Decision

Timeouts are useful for detecting that progress is too slow. They do not reveal which durable decision another process made before it became unreachable.

A participant that has not voted yes may still be able to abandon its local work according to the protocol. A prepared participant has crossed a different boundary: it has promised to follow the final outcome and cannot infer abort from silence alone.

Consider a coordinator that recorded commit, told South and then lost connectivity to North. If North automatically rolls back after thirty seconds, the balances become inconsistent. The timeout would have traded agreement for a guessed outcome.

A recovery process can ask the coordinator's durable decision service, and some protocols can obtain conclusive evidence from other participants. If no accessible source can establish the decision, the uncertainty remains real.

Do not hide that state behind a generic “transaction failed” message. The caller should see pending or outcome unknown when the system has not yet established whether the operation committed.

Recover a Coordinator from Durable State

Suppose the coordinator restarts after recording commit for T42 but before receiving all acknowledgements. It reads its log, finds the committed decision and sends that same decision to participants that still need resolution.

It must not ask the application whether it still wants the transfer. The decision was already made. A cancellation requested afterwards is a new business operation, not permission to rewrite the original outcome.

If the coordinator failed before durably choosing an outcome, its recovery follows the transaction manager's defined protocol. The participant list, recorded votes and logging rules determine what it can safely conclude. Absence of an entry in an unrelated application log is not sufficient evidence.

Repeated recovery messages are expected. Participants and coordinator records need stable transaction identities so retries resolve the same work rather than create another transfer.

Coordinator storage therefore needs a recovery policy as serious as the databases themselves. Losing the only decision log while prepared participants remain can create an operational problem that ordinary application restart procedures cannot resolve.

Distinguish Atomic Commit from Simultaneous Visibility

Two-phase commit aims for one consistent commit-or-abort outcome. It does not make network messages arrive at exactly the same moment or make every independent observer see both databases change simultaneously.

After the coordinator decides commit, South may complete before North receives the message. A separate reader making unrelated queries against both databases can encounter observations from different moments, depending on the databases and read protocol.

Applications needing a consistent cross-database read must design that requirement as well. They may use a transaction system that provides the necessary distributed isolation, read a common operation status or avoid constructing an authoritative result from unrelated snapshots.

Atomicity and isolation are different properties. Two-phase commit coordinates the outcome; concurrency control determines how transactions interact while reading and writing. Combining databases with different isolation behaviour does not automatically create a globally serializable application.

For the credit example, the transfer operation's status can explain that completion is still being delivered. A monitoring page should not declare credits lost by adding values read independently during an unresolved transition without understanding that boundary.

Keep External Effects Outside the Assumed Transaction

Sending an email, calling a payment provider or posting a webhook is not usually a preparable database operation. The remote service may complete an irreversible action as soon as it receives the request.

If the application sends a confirmation email before the coordinated transaction commits, the email can escape even when the databases later abort. If it waits until after commit and crashes before sending, the email can be omitted without a durable dispatch record.

Record the intent to send the email in a transactionally managed outbox where appropriate. After the business transaction commits, a separate worker delivers the message with a documented retry and duplicate-handling policy.

The outbox does not make the email part of 2PC. It preserves the obligation to perform the external action after the database outcome is known. That is a useful, narrower promise.

When evaluating a distributed transaction library, list every resource it actually coordinates. A convenient application API cannot extend transactional guarantees to a third party that does not implement the required participant contract.

Recognise the Cost of Prepared Work

Preparation can hold locks longer than an ordinary local transaction because participants wait for network communication and a shared decision. A slow participant can therefore increase contention in otherwise healthy databases.

For a low-volume administrative operation, that may be acceptable. For a frequently updated inventory record, several milliseconds of additional coordination can matter, and a prolonged outage can block a queue of dependent requests.

Resource costs are not limited to locks. Database versions may retain transaction state or old row versions needed for recovery. Long-lived prepared transactions can interfere with maintenance and exhaust configured limits.

Measure the number and age of prepared transactions, not just the application's average response time. A mostly successful workload can hide a small collection of abandoned preparations that become dangerous later.

Keep coordinated transactions small. Do not prepare while waiting for user input, running an hour-long report or making an unrelated network request. Complete the necessary local work promptly and let the transaction manager resolve the decision without avoidable delays.

Inspect an Unresolved Transaction Carefully

An operator investigating blocked account updates should connect the database's prepared transaction identity to the coordinator's operation record. The useful questions are when it prepared, which participants are involved and whether a final decision exists durably.

PostgreSQL's pg_prepared_xacts view exposes identifiers, preparation times, owners and databases for current prepared transactions. This is diagnostic evidence, not a instruction to roll back every old entry automatically.

If the coordinator recorded commit, recovery should complete commit at the remaining participants. If it recorded abort, the remaining work should roll back. If authoritative decision evidence is unavailable, follow the transaction system's documented recovery procedure.

Manual heuristic decisions can break atomicity: an operator may force one participant to commit while another has already rolled back. Such actions require understanding the transaction manager's support and a business reconciliation plan, rather than being a routine timeout cleanup job.

After resolving the incident, investigate why normal recovery failed. Missing credentials, a renamed database endpoint or deleted coordinator state can turn an otherwise recoverable interruption into a recurring operational problem.

Understand What High Availability Changes

Replicating the coordinator's durable state can make a failed coordinator process easier to replace. A new leader can recover the same decision history instead of relying on the failed machine's local memory.

That improves availability, but it does not remove every blocking condition. The replacement still needs access to authoritative state, and participants may remain unreachable. The replication mechanism has its own quorum and failure assumptions.

Avoid two independent coordinators making conflicting decisions for the same transaction. Failover needs one authoritative decision history and appropriate ownership controls. Copying the coordinator process to another server without coordinating its state can create a more dangerous problem.

Database failover also needs to preserve prepared transaction state under the database's supported configuration. A standby that lacks the necessary durable history cannot safely pretend to be an equivalent replacement merely because it accepts connections.

Test the actual combination of transaction manager, database versions, replication and failover tooling. High availability is a property of the complete recovery path, not an attribute inherited from any one product in the diagram.

Compare the Alternatives by Their Promise

Keeping related data in one database avoids the cross-database commit boundary. If the split is only organisational and creates constant coordination, reconsider whether the data belongs in one transactional unit.

An asynchronous workflow can allow intermediate states, such as transfer requested, debit reserved and credit applied. Recovery retries steps or performs compensating actions. That may keep services available during some failures, but it changes what users can observe.

A saga is a common term for such a multi-step business workflow with compensations. Compensation is a new action, not a magical rewind: a refund may happen later and an email cannot be unsent.

An outbox handles reliable publication of events after a local database transaction. It does not atomically update two independent databases, but it can support an eventual-consistency design when the second database is a projection rather than a peer authority.

Choose the model that matches the invariant. If every intermediate difference is unacceptable and all resources can participate, coordinated transactions may be appropriate. If the product can represent pending work and recovery, a durable workflow may fit better.

Check Whether Your Stack Really Supports Participation

Database support is only one part of an operational 2PC system. Drivers, transaction managers, deployment permissions and managed-service restrictions must work together, including after a process restarts.

MySQL's XA transaction documentation describes its support for participating in distributed transactions. PostgreSQL exposes different local commands. These interfaces should not be mixed through homemade assumptions about identical recovery behaviour.

Review limitations involving statement types, connection lifetimes, authentication and failover. A proof of concept that commits two tiny updates successfully has not yet demonstrated recovery from a lost coordinator or prepared participant.

Also decide who owns the transaction manager operationally. It needs deployment, monitoring, backups and a supported incident procedure. If nobody can explain how an unresolved transaction is recovered at night, the architecture is incomplete.

For most ordinary application code, use a supported integration rather than manually issuing preparation commands from controller methods. The difficult work is the failure protocol and durable state, not the syntax of the final commit command.

Test the Failure Points in a Controlled Environment

Start with two disposable databases and a transaction manager that supports them. Create test accounts with known balances and a stable transfer identifier. Verify both commit and ordinary business rejection before injecting failures.

Interrupt the coordinator before preparation, after one participant prepares, after both prepare and after recording commit but before notifying every participant. Those positions produce different recovery responsibilities.

For each experiment, inspect the operation record, participant states and held resources. Restart the failed component and verify that the final balances match one allowed outcome: either both changes committed or neither did.

Lose the caller's successful response and retry using the same operation identity. The system should return the existing transfer outcome instead of performing another debit and credit. This tests the API boundary that 2PC alone does not solve.

Keep a separate observation for independent readers during decision delivery. It helps the team understand the difference between atomic outcome and the visibility guarantees of its chosen read path.

Give the User a Truthful Operation Status

The transfer API should distinguish a confirmed rejection from an unresolved outcome. If validation rejects insufficient credit before any commit decision, the user can be told the transfer did not proceed. If a response is lost during decision delivery, the application may need to return a pending operation reference instead.

A status endpoint can report the stable transfer identity, requested amount and known outcome. It should use authoritative operation state rather than infer success from whichever balance query responds first. The user can refresh the status without submitting another business operation.

Choose what cancellation means for a pending transfer. Before a final decision, cancellation may request an abort through the transaction manager's supported workflow. After commit has been durably chosen, cancellation cannot reverse that decision; a separate reversal operation is required if the business permits one.

Keep support screens equally precise. An operator should be able to distinguish waiting for a vote, decision recorded, participant recovery pending and completion acknowledged. These are useful operational details even if the customer-facing interface groups several of them under Processing.

Set Limits Before a Small Failure Spreads

Suppose the application allows unlimited transfers while one participant is slow. Other participants may accumulate prepared work faster than recovery can resolve it. Eventually their limits, locks or maintenance requirements become the next incident.

Bound the number of concurrent coordinated transactions and monitor the oldest unresolved age. If a participant remains unavailable, reduce or stop admission for operations that require it while preserving access to unrelated features and operation-status queries.

Do not respond by aggressively retrying every pending transfer with a new transaction identity. That creates more work and can repeat business actions whose outcomes are merely unknown. Recovery should first resolve the original identities using the transaction manager's records.

After the participant returns, drain recovery work at a controlled rate. A sudden wave of commits, rollbacks and application retries can compete with normal traffic. Measure both the number of unresolved transactions and whether the affected user operations are actually completing.

These controls do not change the protocol's agreement guarantee. They keep its resource obligations within the capacity the team can operate and prevent a recoverable interruption from exhausting every healthy component around it.

Summary

Two-phase commit coordinates one decision across transactional participants. First, each database durably prepares and votes. Then the coordinator records a final outcome and delivers it until every participant resolves the same transaction.

The important failure occurs after a participant votes yes but cannot learn the decision. It may have to wait while holding resources because a timeout cannot reveal whether another participant already committed. Durable coordinator state and tested recovery make that uncertainty manageable.

Use the protocol where its atomicity promise and operational costs fit the problem. Keep external effects, API retries and cross-database read consistency explicit, and consider whether a single database or a durable business workflow would provide a simpler appropriate boundary.