How do you handle distributed transactions without two-phase commit?
Two-phase commit (2PC) requires a transaction coordinator across all participants and global locks. In a distributed microservices system this is impractical: failures during phase 2 deadlock services indefinitely.
Modern alternatives:
1. Saga pattern (compensating actions) — already covered. Each step is a local transaction with a compensating action.
2. Outbox pattern — atomically write business state + event in the same DB transaction. A separate publisher reads the outbox and ships events.
public async Task PlaceOrderAsync(Order order, CancellationToken ct) {
using var tx = await db.Database.BeginTransactionAsync(ct);
db.Orders.Add(order);
db.OutboxMessages.Add(new OutboxMessage {
Type = nameof(OrderPlaced),
Payload = JsonSerializer.Serialize(new OrderPlaced(order.Id))
});
await db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
}
// Background service polls and publishes
public class OutboxPublisher : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken ct) {
while (!ct.IsCancellationRequested) {
var pending = await db.OutboxMessages
.Where(m => m.PublishedAt == null)
.Take(50)
.ToListAsync(ct);
foreach (var msg in pending) {
await bus.PublishAsync(msg.Type, msg.Payload, ct);
msg.PublishedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync(ct);
await Task.Delay(1000, ct);
}
}
}
Guarantees at-least-once delivery — consumers must be idempotent.
3. Idempotent operations + retries — design every cross-service call to be safe to repeat. Use an idempotency key:
POST /payments
Idempotency-Key: a4f8...
The receiver stores the key + response. A retry with the same key returns the original response without re-running side effects.
4. Eventual consistency UX — accept that the system is consistent eventually, not immediately. Show "processing" states. Don't show stale "your refund is being processed" if the user expects "balance updated immediately".
The honest answer: there's no free lunch. You trade strong consistency for partition tolerance (CAP). The replacement is harder operationally — idempotency, retries, dead-letter handling, monitoring of unfinished sagas. Plan for it from day one rather than retrofit.