Introduction
The previous articles use local transactions, outboxes, idempotency, and Sagas. A reasonable objection is:
Why accept eventual consistency when a distributed transaction could commit everything together?
Two-phase commit (2PC) is a real protocol with a stronger atomicity goal than a Saga. Under the right resource managers and operating environment, it can be correct and useful.
It rarely fits independently deployed microservices because the atomic unit crosses availability, ownership, deployment, and scaling boundaries. The question is not whether 2PC works. It is whether every participant can safely share its blocking and recovery model.
The Production Problem
An order request must update Orders and Inventory databases. A TransactionScope makes the code look local:
using var scope = new TransactionScope(
TransactionScopeAsyncFlowOption.Enabled);
await orders.SaveAsync(cancellationToken);
await inventory.SaveAsync(cancellationToken);
scope.Complete();
The second durable resource can promote the transaction to a distributed coordinator. Production now depends on:
- both database providers supporting enlistment and promotion;
- coordinator connectivity, identity, firewall, and platform support;
- locks remaining acceptable across network round trips;
- operators recovering in-doubt transactions;
- both service owners accepting one shared commit and failure domain.
If Inventory is an HTTP service or a broker without compatible enlistment, TransactionScope cannot make it a participant. An ambient transaction does not travel through arbitrary JSON requests.
Core Concepts
Phase one: prepare
The coordinator asks every participant whether it can commit. A participant makes its intended changes durable enough to commit later, keeps required resources, and votes yes or no.
After voting yes, it cannot unilaterally decide to roll back without violating atomicity.
Phase two: decision
If every participant votes yes, the coordinator records commit and instructs all participants to commit. Otherwise it instructs rollback.
The protocol coordinates one outcome, but failures can leave participants uncertain. A prepared participant may wait for the coordinator’s recorded decision while retaining locks or prepared state.
Atomicity does not imply availability
2PC prioritizes one commit decision. During coordinator or network failure, it may block progress rather than allow participants to diverge.
That can be correct for a tightly controlled financial database operation. It can be unacceptable for services expected to deploy, scale, and fail independently.
TransactionScope promotion is conditional
System.Transactions can begin with a lightweight local or promotable transaction and escalate when another durable resource enlists. Whether promotion succeeds depends on the resource providers and runtime environment.
Code review must identify actual enlisted resources. “It uses TransactionScope” does not prove the production operation is distributed or supported.
Isolation has a duration cost
A distributed transaction keeps its consistency boundary open across more participants and network latency. Longer lock duration increases contention, deadlock exposure, log retention, and blast radius.
Timeouts limit duration but introduce more aborts and recovery work. They do not make a slow distributed commit cheap.
How It Works
Application -> Coordinator: begin
Application -> Orders DB: write
Application -> Inventory DB: write
Coordinator -> both: prepare?
Orders DB -> Coordinator: yes
Inventory DB -> Coordinator: yes
Coordinator: durably record commit
Coordinator -> both: commit
If the coordinator stops after recording commit but before Inventory receives the decision, Inventory remains prepared until recovery learns the outcome.
If a participant votes no, every prepared participant must roll back. Business workflows cannot selectively keep useful partial work unless that choice exists outside the atomic transaction.
Architecture Diagram
.NET Implementation
Use TransactionScope only after verifying every resource and deployment target.
var options = new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromSeconds(5)
};
using var scope = new TransactionScope(
TransactionScopeOption.Required,
options,
TransactionScopeAsyncFlowOption.Enabled);
await using var ordersConnection =
new SqlConnection(ordersConnectionString);
await ordersConnection.OpenAsync(cancellationToken);
await using var inventoryConnection =
new SqlConnection(inventoryConnectionString);
await inventoryConnection.OpenAsync(cancellationToken);
await WriteOrderAsync(ordersConnection, cancellationToken);
await ReserveInventoryAsync(
inventoryConnection,
cancellationToken);
scope.Complete();
Opening the second enlisted durable connection may promote the transaction. Validate promotion in an environment matching production, including coordinator configuration and failure recovery.
Do not wrap outbound HTTP, SMTP, or a normal broker publish in TransactionScope and assume atomicity. They participate only if their resource manager explicitly supports the transaction protocol used.
With EF Core:
- a single
SaveChangesnormally uses one local transaction; - sharing a transaction across contexts requires compatible provider connections;
- manually initiated transactions interact with retrying execution strategies and must be executed as one retriable unit;
- an ambiguous commit outcome requires a verification strategy.
Inspect distributed transaction creation:
TransactionManager.DistributedTransactionStarted +=
(_, args) =>
{
logger.LogWarning(
"Distributed transaction started: {TransactionId}",
args.Transaction.TransactionInformation
.DistributedIdentifier);
};
Treat this as diagnostic evidence, not the only monitoring mechanism. Provider and coordinator telemetry remain necessary.
Common Mistakes
Assuming ambient means distributed
Ambient context coordinates only enlisted resources. Arbitrary service calls do not enlist.
Discovering promotion in production
A code path may remain local in tests and promote only when a second connection opens under real load. Test the exact connection and provider topology.
Ignoring in-doubt recovery
Prepared transactions can retain resources. Operators need queries, ownership, alerts, and safe resolution procedures.
Crossing service ownership with database credentials
Letting one service directly enlist another service’s database bypasses its API, invariants, deployment boundary, and ownership.
Treating 2PC as a latency optimization
It provides atomic coordination at the cost of additional durable and network steps. It is not a faster alternative to messaging.
Replacing all local transactions
Most operations need only one service-owned transaction. Promotion adds failure modes without benefit.
Best Practices
- Prefer one service-owned local transaction.
- Verify every resource manager’s enlistment and platform support.
- Keep distributed transactions short and bounded.
- Use the weakest isolation level that preserves required invariants.
- Monitor promotion, coordinator health, aborts, duration, and in-doubt participants.
- Document recovery before enabling the first production transaction.
- Load-test lock duration and coordinator failure.
- Do not cross service database ownership to manufacture atomicity.
Observability
Capture:
- local versus promoted transaction count;
- transaction duration, abort, timeout, and heuristic or in-doubt outcomes;
- coordinator availability and recovery backlog;
- participant lock waits and deadlocks;
- prepared-transaction age;
- correlation between application request and coordinator transaction ID.
Avoid logging full connection strings or transaction payloads. Alert on any prepared transaction older than the expected maximum and on unexpected promotion.
Verification
In a production-like environment:
- prove whether the second resource promotes;
- stop a participant before prepare and verify rollback;
- interrupt coordinator connectivity after prepare and verify recovery;
- restart application and coordinator nodes;
- measure lock and latency impact under load;
- verify operating procedures can identify and resolve an in-doubt transaction.
A happy-path commit test is insufficient.
Decision Matrix
| Context | 2PC fit |
|---|---|
| Two supported databases in one tightly controlled application boundary | Possible; measure and operate it |
| Independent microservices with private databases | Poor; use local transactions and messages |
| HTTP service plus database | Unsupported unless an explicit compatible transaction protocol exists |
| Database plus broker with no shared coordinator | Use Transactional Outbox |
| Long-running workflow with human or external steps | Use Saga or durable workflow |
| Low-latency, high-contention path | Usually poor due to coordination and lock duration |
| Strong atomicity legally or financially required and all resources support it | Consider 2PC with explicit availability trade-off |
| Single database | Use a local transaction |
Relationship to Other Patterns
Transactional Outbox keeps atomicity inside one producer database and accepts asynchronous publication.
Saga coordinates local transactions through compensation and explicit intermediate states. It trades isolation for independent availability and business recovery.
Idempotent Consumer handles repeated messages created by at-least-once delivery. Retry and Circuit Breaker manage transient failure but cannot resolve an in-doubt commit decision.
These are not interchangeable implementations of the same guarantee. Choose based on the required consistency boundary and acceptable failure mode.
Decision checklist
- Which exact durable resources enlist?
- Do every provider, runtime, and deployment target support promotion?
- Is atomicity more important than independent availability during partition?
- How long can locks and prepared state remain?
- Who operates the coordinator and resolves in-doubt transactions?
- Does the design cross service-owned database boundaries?
- Would an outbox or Saga express the business failure more honestly?
- Have coordinator and participant failures been tested under load?
Key takeaways
- 2PC is a protocol, not an ambient-context magic trick.
- A prepared participant may block while awaiting the decision.
- HTTP calls do not automatically enlist in
TransactionScope. - Distributed atomicity couples availability and operations.
- Local transactions plus explicit messaging usually match microservice ownership better.