Sagas vs. Two-Phase Commit: Two Fundamentally Different Answers to Distributed Transactions
Coordinated blocking consistency against compensating-action eventual consistency — and why most modern systems quietly picked the second option.
Place an order that touches inventory, payment, and shipping, and each of those services keeps its own database. Something has to guarantee that either all three steps happen or none of them do — otherwise you end up with a charged card and no order, or a reserved item nobody paid for. Two answers to that problem have dominated distributed systems thinking for decades: two-phase commit, which coordinates a single all-or-nothing decision, and the saga pattern, which accepts that things can go temporarily wrong and undoes them afterward. Most teams building microservices today use the second one. Few of them chose it on purpose.
Two-Phase Commit: Consistency Through Blocking
Two-phase commit works exactly the way its name suggests. A coordinator asks every participant to prepare — lock the relevant resources and confirm they’re ready to commit — and only once every participant votes yes does the coordinator send the actual commit instruction. It’s the mechanism behind the XA specification, which is what lets a Java service coordinate a PostgreSQL database and a message queue under one UserTransaction, and it’s been the standard answer to distributed atomicity since 1991.
The catch is what happens between the two phases. Every participant that voted yes is now holding locks, waiting for a decision it can’t make on its own. If the coordinator crashes before sending that decision, those locks stay held — sometimes indefinitely — because no participant is allowed to guess. An analysis from Alibaba Cloud’s engineering team describes this plainly: the more resource managers a transaction spans, the longer the blocking window and the higher the odds of a deadlock, which is exactly why 2PC struggles once it crosses service boundaries rather than staying inside one database.
2PC hasn’t disappeared, though — it just retreated to places where one team controls both ends. Distributed SQL databases like CockroachDB and Google Spanner still run two-phase commit internally at the storage layer, and Kafka’s transactional producer uses a comparable two-phase handshake to guarantee a batch of messages is published as a unit. In both cases, the coordination happens inside a single system that one team owns end to end, which is a very different bet than coordinating across independently deployed services.
Sagas: Consistency Through Compensation
The saga pattern starts from a different premise: don’t try to hold a global lock across services you don’t jointly control. Instead, break the operation into a sequence of local transactions, each committed independently, and if a later step fails, run compensating actions that undo the earlier ones. Reserve the inventory, charge the card, create the shipment — and if the shipment step fails, release the inventory and refund the charge instead of rolling back a transaction that already committed.
Nothing is locked while that sequence runs. Each service commits its own local transaction the moment it’s ready, using nothing more exotic than its own database’s normal transaction support. The price is that other requests can briefly observe an inconsistent state — inventory decremented before the payment is confirmed — and the application has to be built to tolerate that window rather than pretend it doesn’t exist.
| Consideration | Two-Phase Commit | Saga Pattern |
|---|---|---|
| Consistency model | Strong — all-or-nothing at commit time | Eventual — corrected via compensation if a step fails |
| Behavior on coordinator failure | Participants block, holding locks until resolved | No coordinator lock to hold; steps proceed or compensate independently |
| Isolation | Full — no other transaction sees intermediate state | None by default — intermediate state is visible mid-saga |
| Cross-team / cross-vendor fit | Poor — needs shared XA support across every participant | Good — each service only needs its own local transaction |
| Implementation cost | Low application code, high operational risk | Higher application code — every step needs a compensating action |

Why Most Systems Chose Sagas Without Saying So
Here’s the part rarely said out loud: most teams didn’t sit down, weigh consistency models, and select eventual consistency as a deliberate architectural principle. They chose microservices for independent deployability, gave each service its own database because sharing one defeats the purpose, and only afterward ran into the fact that XA transactions across independently owned, independently deployed services are operationally miserable — different vendors, different failure domains, locks held across a network hop that might time out at any point. Two-phase commit didn’t get rejected on paper. It got avoided in practice, one incident at a time, until “publish an event and let each service react” became the default without anyone writing that decision down as a saga.
That’s also why so many saga implementations in the wild predate the term itself. A retry queue here, a cleanup job there, an event that triggers a refund when a downstream step fails, teams built compensating logic because it was the only workable option, then later recognized the pattern already had a name.

The Dissenting View: 2PC Isn’t Dead
Not everyone treats this as a settled argument. Vendors who still build XA-based transaction managers point out that sagas buy availability by giving up isolation — a saga in progress can be read mid-flight, and a compensating action is a best-effort undo, not a guarantee that nothing downstream already acted on the dirty state. Atomikos makes this case directly, arguing that within a single team’s ownership boundary, an optimized 2PC implementation can avoid the single-point-of-failure and blocking problems that gave the protocol its reputation, and that some teams adopted sagas less because 2PC was unworkable and more because they’d been told it was.
Questions worth answering before picking either pattern
- Does one team own every participant, or does the transaction cross ownership and vendor boundaries?
- Can the business genuinely tolerate a brief window of inconsistency, or does a regulator or ledger require otherwise?
- Is there a realistic compensating action for every step, including the ones that are hard to undo, like sending an email or charging a card?
- What happens to a saga that fails halfway and never receives its compensating event — is there a timeout and a reconciliation job?
- Would an optimized, single-vendor 2PC implementation actually avoid the blocking problems, or is that assumption untested for your stack?
What We Learned
Two-phase commit and the saga pattern aren’t really competing implementations of the same idea — they’re two different bets about where consistency should be enforced. 2PC bets on a coordinator that can make every participant wait until a single decision is safe to make, which works well when one team owns the whole path and badly once it crosses service and vendor boundaries. Sagas bet that local transactions plus compensating actions are good enough, trading isolation for availability and independence. Most microservice teams ended up on the saga side of that bet not because they evaluated both and chose deliberately, but because 2PC’s operational cost became unworkable first, which is worth remembering the next time “eventually consistent” gets treated as an architectural virtue rather than the fallback it usually started out as.



