# Two-Phase Commit Alternatives: Outbox and Sagas

> Two-phase commit coordinates atomic commits, but prepared transactions can block while the decision is unavailable. Compare its recovery costs with the weaker transaction boundaries of outboxes and sagas, including intermediate states and compensation.

Published: 2026-10-27. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/two-phase-commit-alternatives

**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:

1. **Prepare:** each participant durably prepares its changes and votes yes or no.
2. **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.

![Sequence diagram of two-phase commit: the coordinator sends prepare to both databases, each votes yes while holding locks, then the coordinator sends commit to both. A crash between phases leaves participants in doubt.](https://milanjovanovic.tech/blogs/articles/two-phase-commit-alternatives/two-phase-commit-sequence.png)

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**](https://www.postgresql.org/docs/current/sql-prepare-transaction.html) 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**](https://learn.microsoft.com/en-us/dotnet/api/system.transactions.transactionmanager.implicitdistributedtransactions) 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**](https://milanjovanovic.tech/blog/transactional-outbox-ef-core) 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**](https://milanjovanovic.tech/blog/idempotent-consumer-handling-duplicate-messages), 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**](https://milanjovanovic.tech/blog/saga-pattern-dotnet) 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:

1. Create an order in a pending state.
2. Charge the payment method.
3. Reserve inventory.
4. 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.

![Saga flow: create order, charge card, reserve stock run as local transactions. On success the order completes; if reserving stock fails, compensating actions refund the charge and cancel the order in reverse order.](https://milanjovanovic.tech/blogs/articles/two-phase-commit-alternatives/saga-compensation-flow.png)

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**](https://milanjovanovic.tech/blog/orchestration-vs-choreography) 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**](https://milanjovanovic.tech/blog/eventual-consistency-dotnet) 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**](https://cloud.google.com/spanner/docs/transactions) 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**](https://milanjovanovic.tech/blog/distributed-transactions-vs-outbox) 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.
