Two-phase commit coordinates an atomic commit across multiple participating resources. Its main limitation is that prepared transactions can block while the commit decision is unavailable. The outbox and saga patterns avoid that cross-resource commit boundary, but they require explicit intermediate states, reliable messaging, and recovery rather than equivalent distributed ACID guarantees.
How Does Two-Phase Commit Work?
Imagine two databases that must agree on the outcome of a transaction. A coordinator runs the commit protocol:
- Prepare: each participant durably prepares its changes and votes yes or no.
- Decide: the coordinator durably records commit if all participants voted yes, or rollback otherwise, then sends that decision to the participants.
A participant that voted yes has promised it can commit later. It must preserve that promise across a crash, typically by retaining transaction records and locks. The coordinator must also recover its decision after a restart.
This protocol provides atomic commit for participating transactions. Isolation remains a separate concern, and an HTTP service that cannot enlist does not become a participant merely because the caller wraps it in a transaction.
Why Prepared Transactions Can Block
Suppose both databases prepare successfully and the coordinator records a commit decision. One database receives the decision, but the other loses contact with the coordinator. The second database cannot safely decide to roll back on its own: the first may already have committed.
This is an in-doubt transaction. It waits for a recoverable decision, and locks on the affected data can obstruct other work. The impact depends on which rows are locked and what other requests need them; it does not automatically stop every operation in the system.
Even without a failure, coordination extends transaction duration. A slow participant keeps the other participants waiting, increasing contention on frequently updated data. Coordinator replication and recovery procedures help, but the application must still plan for partitions and in-doubt transactions.
PostgreSQL's prepared transaction documentation warns against leaving transactions prepared for long periods because they retain locks and interfere with cleanup. Prepared transactions need an external transaction manager that reliably finishes them.
.NET Support Is Provider and Platform Specific
TransactionScope can represent a local transaction or a distributed one.
Promotion depends on the resources enlisted and the provider's behavior.
It is not a general-purpose transaction wrapper for arbitrary databases, message brokers, and HTTP APIs.
Modern .NET supports distributed System.Transactions on Windows starting with .NET 7.
Implicit promotion is disabled by default and requires TransactionManager.ImplicitDistributedTransactions = true.
The Microsoft documentation describes these constraints.
If you deploy Linux containers, that Windows System.Transactions mechanism is unavailable.
Other distributed databases may implement their own transaction protocol internally; the runtime restriction does not mean 2PC itself is Windows-only.
Alternative 1: The Transactional Outbox
Many application requirements are narrower than an atomic cross-database transaction. They need a business write to reliably cause a message.
For example, save an order and an OrderPlaced message in the same database transaction.
A worker then publishes the committed message to the broker.
If the broker is unavailable, the order can remain committed while the message waits in the outbox.
The EF Core outbox walkthrough provides a complete PostgreSQL and MassTransit example.
The critical ordering is to capture the outgoing message before the same SaveChangesAsync call that persists the business change.
Saving first and publishing afterward leaves a failure window.
The outbox guarantees atomic persistence of the business data and the intent to publish. Delivery requires retry, durable storage, and recovery of failed messages. It also requires idempotent consumers, because a crash after publish but before recording success causes redelivery.
This changes the transaction boundary. The producer does not wait for every consumer to update its own database. If your requirement is "inventory must already be reserved when the order response returns," an outbox alone does not meet it.
Alternative 2: Sagas
A saga coordinates a workflow made from local transactions. Each step commits in its own service, and subsequent messages move the workflow forward. When a step cannot complete, the workflow invokes compensating actions for earlier completed work.
Consider this order flow:
- Create an order in a pending state.
- Charge the payment method.
- Reserve inventory.
- Confirm the order.
If inventory reservation definitively fails after a successful charge, refund the charge and cancel the order. If the charge times out, its outcome is unknown: query or reconcile with the payment provider before deciding whether to retry or compensate.
A refund is another business transaction. It can fail, and it may not erase fees, notifications, or other effects of the original charge. Compensation restores an acceptable business state rather than reversing time.
Orchestration stores the workflow state in a coordinator that sends commands and handles results. Choreography distributes the next-step decisions across event handlers. The orchestration versus choreography comparison covers the coordination and observability tradeoffs.
Both need durable messages and idempotent steps. Persist saga transitions and outgoing commands together using an outbox, and retain enough state to recover after a restart. Monitor sagas that remain pending beyond their expected deadline.
What These Alternatives Do Not Provide
Outboxes and sagas use local transactions, so they can still block on local locks or an unavailable database. They reduce the scope of coordination; they do not make every operation always available.
They also expose intermediate states.
Users may see PaymentPending or CancellationPending while downstream work is incomplete.
Design the eventual consistency window into the API and user interface instead of promising immediate completion.
Some invariants cannot tolerate those states. If two operations must be atomic, first consider whether they belong in the same service and database. Splitting an invariant across services creates a coordination requirement that messaging cannot remove.
When 2PC Still Fits
Two-phase commit remains useful when all resources support it and atomic commit is required. That includes some enterprise deployments and distributed databases that manage the coordinator and participants as part of the database system.
For example, Spanner's transaction documentation describes distributed transactions using two-phase commit across participating splits. Its database-managed implementation is a different operational proposition from assembling arbitrary independent services under a transaction scope.
The choice is therefore about the required guarantee and infrastructure support. Compare the failure behavior and recovery process, not just the number of application code lines. The distributed transactions versus outbox comparison maps those differences directly.
Summary
For writes within a database, start with a local transaction. For a write that must reliably publish a message, use a transactional outbox. For a workflow that spans local transactions, define saga states, compensation, deadlines, and uncertain outcomes.
When those weaker boundaries do not satisfy the invariant, change the service boundary or evaluate a supported distributed transaction system. The decision should state exactly what a caller can observe during failure and how the system returns to an acceptable state.
Frequently Asked Questions
Why is two-phase commit rarely used in modern systems?
Across independent services, two-phase commit requires compatible transaction infrastructure and recovery of prepared transactions. Outboxes and sagas are alternatives when the workflow can accept intermediate states; they do not provide the same distributed atomicity.
Does .NET still support distributed transactions?
Modern .NET supports distributed System.Transactions on Windows starting with .NET 7. Implicit promotion requires an explicit opt-in, compatible providers, and configured transaction infrastructure. That mechanism is not available on Linux.
What is the difference between the outbox pattern and a saga?
The outbox atomically stores a business write and the intent to publish, with delivery happening later. A saga coordinates a multi-step process across local transactions and defines compensating actions for completed steps when recovery requires them.
Is eventual consistency safe for business-critical operations?
It can be appropriate when the business permits intermediate states and has a defined recovery process. Keep strict invariants inside an atomic boundary, use idempotent steps, and reconcile uncertain outcomes before compensating.

