# Distributed Transactions vs Outbox Pattern

> Two-phase commit coordinates an atomic commit across participating resource managers, while the outbox keeps one local transaction and publishes later. The practical choice depends on platform support, availability requirements, and whether the workflow can tolerate intermediate states.

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

Canonical: https://milanjovanovic.tech/blog/distributed-transactions-vs-outbox

**Two-phase commit** coordinates one atomic outcome across participating resource managers.
The **transactional outbox** commits business data and an outgoing message in one database, then publishes the message asynchronously.
Choose between them by identifying the invariant you need: atomic cross-resource commit and reliable message delivery provide different guarantees.

## Which Transaction Boundary Do You Need?

Suppose placing an order changes the Orders database and publishes `OrderPlaced` to RabbitMQ.
Those are independent operations.
A crash after the database commit but before publishing leaves downstream services unaware of the order.

The [**outbox pattern**](https://milanjovanovic.tech/blog/outbox-pattern-dual-write-problem) solves that dual-write problem.
It does not atomically reserve stock in another service or charge a card.
Those steps belong to a separate business workflow.

If the requirement is instead "two participating databases must commit this transaction together," a distributed transaction may fit.
Both databases need compatible transaction infrastructure, and the application must accept the coordination cost.
If all writes fit in one database transaction, use that local transaction first.

## What Two-Phase Commit Coordinates

In **2PC**, a coordinator asks each participant to prepare its transaction.
A participant that votes yes records enough durable information to finish later and usually retains locks protecting its changes.
It cannot independently abandon the prepared transaction just because the coordinator becomes unreachable.

Once every participant votes yes, the coordinator durably records the commit decision and tells participants to commit.
If a participant votes no, the coordinator chooses rollback.
Recovery uses the recorded decision to finish participants that did not receive the original message.

![Two-phase commit sequence where a coordinator asks the Orders and Inventory databases to prepare and lock resources, then tells both to commit once every participant votes yes](https://milanjovanovic.tech/blogs/articles/distributed-transactions-vs-outbox/two-phase-commit.png)

Atomic commit is not the same as every client observing all changes at the same instant.
Read isolation and how clients access multiple databases still matter.
2PC also cannot enlist an arbitrary HTTP payment API simply because the call occurs inside your C# transaction scope.

A short loop over `PrepareAsync` and `CommitAsync` is not a usable 2PC implementation.
It omits the durable decision log, recovery, message retries, and the treatment of transactions whose outcome is unknown.
Use the protocol implementation supported by your resource managers.

## The .NET Platform Constraint

`TransactionScope` represents an ambient transaction; it does not mean every scope is distributed.
Enlisting another durable resource can cause promotion, depending on the provider and connections involved.
Local transactions can remain local.

For modern .NET, distributed `System.Transactions` support arrived in .NET 7 and is Windows-only.
Implicit promotion also requires explicitly enabling `TransactionManager.ImplicitDistributedTransactions`.
The [**Microsoft API documentation**](https://learn.microsoft.com/en-us/dotnet/api/system.transactions.transactionmanager.implicitdistributedtransactions) documents that opt-in and its platform restriction.

Verify support in every database provider as well as the runtime.
A database supporting prepared transactions does not prove that its .NET provider, deployment platform, and recovery configuration support your proposed distributed transaction.
See [**EF Core's transaction limitations**](https://learn.microsoft.com/en-us/ef/core/saving/transactions#limitations-of-systemtransactions).

## What Failure Looks Like With 2PC

Prepared participants can become **in doubt** when they cannot obtain the coordinator's decision.
Locks on the affected data may remain held until recovery resolves the transaction.
Unrelated data and unrelated transactions do not necessarily stop.

This is the availability cost: a transaction's progress depends on its participants and the decision being reachable.
Coordinator replication and recovery reduce operational risk, but do not make network partitions disappear.
The [**two-phase commit alternatives**](https://milanjovanovic.tech/blog/two-phase-commit-alternatives) article follows this failure case in more detail.

Long network round trips also extend transaction duration.
That matters most when many requests contend for the same rows.
Measure contention under the actual deployment conditions instead of treating distributed transactions as either always too slow or automatically safe.

## What the Outbox Coordinates

The outbox has a smaller transaction boundary:

1. Insert or update the business data.
2. Insert the outgoing message into the same database transaction.
3. Commit the transaction.
4. Let a dispatcher publish the committed message.
5. Record successful delivery so normal polling no longer selects it.

The request can complete while the broker is unavailable, as long as the database remains available and the outbox has capacity.
Downstream services catch up after delivery resumes.
That interval is an [**eventual consistency window**](https://milanjovanovic.tech/blog/eventual-consistency-dotnet), with no automatic upper bound.

The [**EF Core transactional outbox example**](https://milanjovanovic.tech/blog/transactional-outbox-ef-core) includes the packages, PostgreSQL configuration, message contracts, endpoint, and MassTransit model setup.
Its scoped `IPublishEndpoint` captures the message before `SaveChangesAsync`, so the business write and outgoing message commit together.
The configured delivery service publishes afterward.

That ordering is essential.
Calling `SaveChangesAsync` first and publishing afterward through an ordinary endpoint retains the original dual-write bug.
Adding an explicit database transaction around those two calls does not make a broker publish part of that transaction.

## Delivery and Consumer Guarantees

If the dispatcher publishes a message and crashes before recording success, recovery publishes it again.
This is **at-least-once delivery**, assuming retry and recovery continue.
Consumers must tolerate duplicates.

Persist a deduplication record and the consumer's database changes in the same transaction.
A unique constraint must prevent concurrent deliveries from both applying the operation.
The [**inbox pattern**](https://milanjovanovic.tech/blog/implementing-the-inbox-pattern-for-reliable-message-consumption) describes this boundary.

MassTransit's bus outbox does not automatically deduplicate every downstream consumer.
Its consumer outbox is a separate receive-endpoint configuration, and even that cannot make an external HTTP side effect atomic with a database commit.
Use the external system's idempotency support where available and reconcile uncertain outcomes.

## Compare the Actual Tradeoffs

| Concern | Two-phase commit | Transactional outbox |
| --- | --- | --- |
| Atomic boundary | Participating resource transactions | Business write and outgoing message in one database |
| Broker outage | Can prevent a participating transaction from completing | Publishing can lag while local writes continue |
| Failure recovery | Resolve prepared transactions from the durable decision | Retry pending delivery and handle duplicates |
| Consumer completion | Only covered if that resource participates | Outside the producer transaction |
| Infrastructure | Compatible enlistment and recovery support | Transactional storage for business data and messages |
| Operational work | Coordinator recovery, timeouts, lock contention | Outbox lag, retry handling, retention, idempotency |

The outbox avoids a cross-resource commit protocol, but it still needs database transactions and durable infrastructure.
It can run out of storage during a long broker outage, and a poison message can block an ordered stream.
Those are capacity and recovery requirements you must plan for.

## Where Sagas Fit

An order workflow might reserve inventory, request payment, and confirm the order.
When these steps belong to separate services, the [**saga pattern**](https://milanjovanovic.tech/blog/saga-pattern-dotnet) tracks progress and defines compensation.
It still uses eventual consistency.

If payment is definitively declined after stock was reserved, release the reservation and cancel the order.
If the payment request times out, first determine whether the payment succeeded.
A timeout does not establish whether you should retry, refund, or cancel.

Compensation is a new business operation, so it can fail and require retry or manual intervention.
An outbox helps each service publish its result reliably; it does not choose the business response to that result.

## Summary

Use a local transaction when it can protect the invariant.
Use an outbox when a committed change must reliably produce a message.
Consider 2PC when atomic commit across compatible resources is an explicit requirement and its recovery cost is acceptable.

For a multi-service workflow, model pending states, uncertain outcomes, and compensation before writing the handlers.
That decision determines what users can observe while the system is recovering.

## Frequently asked questions

### What is two-phase commit (2PC)?

A protocol where a coordinator first asks every participant to prepare (locking resources), then tells all of them to commit or roll back. It coordinates atomic commit across participating resources, at the cost of coordination and possible blocking during recovery.

### Why is the outbox pattern preferred over distributed transactions in microservices?

The outbox pattern needs only a local database transaction: the state change and the outgoing event commit atomically in one database, and a background process publishes the event afterward. It requires transactional storage for the business write and message, while broker delivery happens separately.

### Does the outbox pattern guarantee exactly-once delivery?

No. Retrying delivery can publish a message more than once. The publisher can crash after sending but before marking a message processed, causing a duplicate. Consumers must be idempotent to handle redelivery.

### Are distributed transactions ever the right choice?

Occasionally: within a single trust boundary where all participants support the protocol (for example, two SQL Server databases with MS DTC) and strong immediate consistency is a hard requirement. Across heterogeneous microservices, they are rarely practical.

### What is the difference between the outbox pattern and a saga?

The outbox reliably publishes events alongside a local state change. A saga coordinates a multi-step business process across services, using compensating actions to undo earlier steps when a later one fails. Sagas typically use an outbox internally for reliable messaging.
