from the 2PC protocol to the saga alternative
Once a transaction spans multiple independent databases or services, a simple COMMIT no longer guarantees atomicity. Two-phase commit solves this through a coordinator-based prepare-and-commit protocol, but it has structural weaknesses that lead most large, distributed systems today to prefer the saga pattern with compensating actions instead.
Table of Contents
- 1. What distributed transactions are and why you need them
- 2. The 2PC protocol: prepare phase
- 3. The 2PC protocol: commit phase
- 4. XA transactions as a standard
- 5. Failure modes: coordinator crash and the blocking problem
- 6. Why 2PC is rarely used at scale
- 7. Saga pattern as an alternative
- 8. Comparison: 2PC vs. saga
- 9. Distributed transaction strategies compared
- 10. Summary
- 11. FAQ
1. What distributed transactions are and why you need them
A distributed transaction coordinates changes across multiple independent, transactional resources, typically several databases, message queues, or services, so that all participating resources either commit together or roll back together. An ordinary local transaction guarantees atomicity only within a single database connection, once two separate databases are involved, there is no native COMMIT that finishes both at once and atomically.
The need for distributed transactions typically arises in architectures with several separate database systems, for example when an order system in one database and an accounting system in another database must be updated consistently at the same time. Without coordination, an order could be created successfully while the corresponding accounting entry fails, leaving inconsistent state between the two systems that would need manual correction.
The classic protocol for solving this problem is two-phase commit, or 2PC, a consensus protocol that uses a central coordinator to bring all participating resources to a shared outcome in two consecutive phases. 2PC is mathematically elegant and, under certain assumptions, guarantees true atomicity across system boundaries, but it has practical properties that significantly limit its use at scale.
2. The 2PC protocol: prepare phase
The first phase of two-phase commit is called the prepare phase, or voting phase. The coordinator sends every participant, called a resource manager in 2PC terminology, a request asking whether it is ready to commit its local transaction. Each participant then performs all necessary checks, constraints, lockability of affected resources, storage space, and on a positive result, writes its changes fully into a durable log, without making them finally visible yet. It then responds to the coordinator with Yes, I'm ready, or No, I cannot commit.
What matters most in this phase is that a participant who answered Yes commits to actually applying its prepared changes later on instruction, no matter what happens afterward, even if its connection to the coordinator drops in the meantime. This commitment is the core that gives 2PC its atomicity guarantee, but it is also the source of the blocking problem described later.
-- Standard XA syntax across participating databases (conceptual)
-- Coordinator instructs each resource manager to prepare its branch
-- Resource manager A (orders database)
XA START 'tx-42:branch-a';
UPDATE orders SET status = 'confirmed' WHERE id = 1001;
XA END 'tx-42:branch-a';
XA PREPARE 'tx-42:branch-a';
-- Changes are durably logged but not yet visible, branch A votes "ready"
-- Resource manager B (accounting database)
XA START 'tx-42:branch-b';
INSERT INTO ledger_entries (order_id, amount) VALUES (1001, 249.00);
XA END 'tx-42:branch-b';
XA PREPARE 'tx-42:branch-b';
-- Changes are durably logged but not yet visible, branch B votes "ready"
-- Coordinator now waits for votes from all branches before proceeding
3. The 2PC protocol: commit phase
Only once the coordinator has received a positive vote from all participants does the protocol move to the second phase, the commit phase. The coordinator first writes its own decision, usually commit, into its own durable log, this moment counts as the actual commit point of the entire distributed transaction. It then sends every participant the instruction to finally commit its prepared transaction, and each participant makes its changes visible and confirms completion to the coordinator.
If even a single participant answered No in the prepare phase, or one is not reachable in time, the coordinator instead decides to abort and instructs every participant to discard its prepared changes. This all-or-nothing behavior is exactly the atomicity guarantee distributed transactions promise: either all participating systems commit together, or none of them do.
-- Coordinator received "ready" from both branch A and branch B, proceeds to commit
-- Resource manager A
XA COMMIT 'tx-42:branch-a';
-- Order status change becomes visible
-- Resource manager B
XA COMMIT 'tx-42:branch-b';
-- Ledger entry becomes visible
-- If either branch had voted "no" in the prepare phase, the coordinator
-- would issue XA ROLLBACK to both branches instead, discarding all changes
4. XA transactions as a standard
XA is the Open Group's standardized industry protocol for distributed transactions, defining how a transaction coordinator communicates with multiple resource managers through a uniform interface. Nearly all relational databases, PostgreSQL, MySQL, Oracle, SQL Server, support XA transactions, as do many message queue systems, which in theory makes it possible to coordinate database operations and message delivery in a single atomic distributed transaction.
In practice, the coordinator itself is usually handled by an application server or a dedicated transaction manager, not by the databases themselves, which merely fill the role of resource manager. This separation allows XA-capable resources of very different kinds to be coordinated within the same distributed transaction, but it also requires the coordinator itself to be implemented as highly available and crash-safe, since it is the single point of truth for the final commit decision.
5. Failure modes: coordinator crash and the blocking problem
The structural problem of two-phase commit shows up at exactly the moment the prepare phase forces: a participant that has already answered Yes must hold its locks on the affected resources until it receives the coordinator's final decision. If the coordinator crashes after collecting the votes but before distributing its decision to all participants, those participants get stuck in an undecided state, their locks remain active, and every other transaction that needs the same resources gets blocked as well.
This so-called blocking problem is not an implementation weakness but a structural property of the 2PC protocol itself: a participant cannot decide on its own whether to commit or roll back, because it does not know how the other participants voted. Only once the coordinator becomes available again, or manual intervention occurs, can the blocked resources be released. In production environments, a coordinator crash during a running 2PC transaction can therefore mean minutes or even hours of lock time on critical resources until the state is resolved manually.
-- After a coordinator crash, a prepared but undecided branch survives a restart
-- and must be resolved manually or through recovery tooling
-- List all prepared transactions still awaiting a final decision
SELECT gid, prepared, owner, database FROM pg_prepared_xacts;
-- Manual resolution once the correct outcome is known from coordinator logs
COMMIT PREPARED 'tx-42:branch-a';
-- or, if the coordinator's log shows an abort decision:
-- ROLLBACK PREPARED 'tx-42:branch-a';
-- Until this runs, the branch keeps holding its locks indefinitely
6. Why 2PC is rarely used at scale
Two-phase commit scales poorly because every participating resource holds locks for the entire duration of the protocol, from the prepare phase to the final commit confirmation. The more participants are involved in a distributed transaction and the higher the network latency between them, the longer the whole process takes and the longer resources stay blocked for other transactions. In systems with high concurrency and geographically distributed databases, this effect quickly becomes the limiting factor for overall throughput.
On top of that comes operational complexity: an XA coordinator must itself be highly available, its decision log must be persisted durably and crash-safely, and in failure cases you need recovery mechanisms that can detect and resolve orphaned, undecided transactions. Cloud-native databases and modern distributed architectures with microservices therefore often deliberately avoid 2PC, because the combination of blocking risk, latency overhead, and operational complexity no longer justifies the benefit of true atomicity in most business applications. This is exactly where the saga pattern steps in as a more pragmatic alternative.
7. Saga pattern as an alternative
The saga pattern deliberately gives up the strict atomicity guarantee of distributed transactions and replaces it with a sequence of local transactions, each committing independently, combined with compensating actions that, on failure, deliberately undo steps that already succeeded. Instead of a global coordinator holding locks across all participants, every step of a saga runs as its own short local transaction that commits immediately and releases its locks right away.
If a step fails, the saga calls the matching compensating action for every previously successful step, for example a cancellation entry instead of a real rollback. For the order example above, this would mean: if the accounting entry fails, a compensating action cancels the order already created, instead of trying to roll back a global transaction spanning both systems. The advantage: no long-lived locks, no dependency on a highly available global coordinator, significantly better scalability across service boundaries.
-- Saga pattern: each step is its own short, independently committed local transaction
-- Pseudocode orchestration around per-service local transactions
-- Step 1: orders service, its own local transaction, commits immediately
BEGIN;
INSERT INTO orders (id, status) VALUES (1001, 'pending');
COMMIT;
-- Step 2: accounting service, its own local transaction, commits immediately
BEGIN;
INSERT INTO ledger_entries (order_id, amount) VALUES (1001, 249.00);
COMMIT;
-- Suppose this step fails due to a business rule violation
-- Compensating action instead of a distributed rollback:
-- undo step 1 with its own local transaction
BEGIN;
UPDATE orders SET status = 'cancelled' WHERE id = 1001;
COMMIT;
-- No global lock was ever held across both services during this process
8. Comparison: 2PC vs. saga
The fundamental difference between two-phase commit and the saga pattern lies in the consistency model: 2PC guarantees strong, immediate consistency across all participating systems, at the cost of locks, latency, and blocking risk. Saga delivers eventual consistency, the systems are temporarily visible in an inconsistent intermediate state, for example an order without its matching accounting entry for a brief moment, but they are guaranteed to converge to either a fully successful or fully compensated end state.
This eventual consistency demands more care from application code: every intermediate state must be acceptable from a business perspective, and a correct, idempotent compensating action must exist for every step. In exchange, the operational complexity of a highly available XA coordinator disappears entirely, and overall scalability increases substantially, because no resource stays locked longer than the duration of its own short local transaction.
-- Idempotent compensating action: safe to execute more than once
-- Using a status check prevents double-cancellation from a retried saga step
UPDATE orders
SET status = 'cancelled'
WHERE id = 1001
AND status <> 'cancelled'; -- no-op if already compensated, safe to retry
-- Idempotent forward step, guarded by a unique constraint on order_id
INSERT INTO ledger_entries (order_id, amount)
SELECT 1001, 249.00
WHERE NOT EXISTS (
SELECT 1 FROM ledger_entries WHERE order_id = 1001
);
9. Distributed transaction strategies compared
The following table summarizes the key differences between two-phase commit and the saga pattern and shows when each approach is the right choice.
| Criterion | Two-phase commit | Saga pattern | Consequence |
|---|---|---|---|
| Consistency model | Strong, immediate consistency | Eventual consistency | Saga needs business-acceptable intermediate states |
| Lock duration | Across the entire protocol duration | Only per individual local transaction | Saga scales significantly better |
| Error handling | Automatic rollback of all participants | Manually defined compensating actions | Saga requires more development effort |
| Coordinator crash | Blocking problem, resources stay locked | No global coordinator needed | Saga avoids a single point of failure |
| Typical use | Few, stable, homogeneous systems | Many, heterogeneous microservices | Choice depends on system landscape |
The table makes it clear: two-phase commit is not fundamentally obsolete, but in modern, distributed microservice landscapes with many participants and variable network latency, its structural drawbacks usually outweigh the benefits of strong consistency.
Mironsoft
Distributed systems, transaction architecture, and microservice consistency
Consistency across multiple systems without global locks?
We analyze your distributed architecture, assess whether 2PC or the saga pattern fits your system landscape, and implement robust compensation logic for consistent processes across service boundaries.
Architecture review
Assessing your distributed transaction needs and system boundaries
Saga implementation
Building orchestration and compensation logic for microservice processes
XA advisory
Setting up XA transactions cleanly where strong consistency is genuinely required
10. Summary
Distributed transactions solve a fundamental problem of distributed systems: guaranteeing atomicity across multiple independent resources. Two-phase commit achieves this through a coordinator that gathers agreement from all participants in a prepare phase and only then finally commits in the commit phase. The protocol is mathematically sound but has a structural blocking problem on coordinator crash and scales poorly in systems with many participants or high latency due to long lock times.
The saga pattern gives up strong, immediate consistency in favor of eventual consistency and replaces the global coordinator with a sequence of short, independently committing local transactions with defined compensating actions for the failure case. In modern microservice architectures with many heterogeneous systems, this more pragmatic alternative is usually the better choice, while 2PC remains justified in smaller, more stable system landscapes with few participants.
Distributed transactions and two-phase commit: the essentials at a glance
2PC protocol
Prepare phase gathers agreement from all participants, commit phase carries out the final decision, all or nothing.
Blocking problem
If the coordinator crashes after the prepare phase, participants holding locks get stuck in an undecided state.
Saga pattern
Short, independently committing local transactions plus compensating actions instead of global locks.
Decision criterion
Few, stable systems: 2PC acceptable. Many heterogeneous microservices: saga usually the better choice.