Two Phase Commit vs Saga Pattern for Distributed Transactions

When your whole application talks to one database, transactions are easy. A customer places an order, you wrap charging the card, reserving inventory, and recording the ledger entry in a single transaction. If any step fails, the database rolls everything back and it is like nothing happened. This is what database transactions promise, atomicity and isolation, and a single database gives it to you for free.

That guarantee disappears the moment you split into microservices. Once each service owns its own database, the same operation is now three completely separate writes to three completely separate systems, on three separate machines. If the card charge commits and the inventory reservation fails because an item is out of stock, there is no database level rollback that can undo a charge that already committed on a different machine. This is a distributed transaction, a single logical operation that spans multiple independent databases where every step needs to succeed together or be cleaned up when something goes wrong.

There are two established answers to this problem, and the industry has overwhelmingly settled on one of them for a specific reason.

Two Phase Commit

Two Phase Commit, usually shortened to 2PC, is the textbook answer. It introduces a coordinator whose only job is making sure every participant in a transaction agrees on the outcome before any of them makes it permanent.

Coordinator Card Service Inventory Service Ledger Service Phase 1: prepare (can you commit?) all vote yes, rows locked and held Coordinator crashes here Phase 2 commit message never sent All three services stay blocked, locks held

In the prepare phase, the coordinator asks every participant if it can commit. Each service does the actual work, durably records the change, locks the affected rows so nothing else can touch them, and responds yes or no. If every participant votes yes, the coordinator moves to the commit phase and tells everyone to make it permanent. If even one participant votes no, the coordinator tells everyone to abort and release their locks.

Used in: distributed databases where the coordinator and participants are part of the same system, such as Google Spanner and YugabyteDB.

Advantages:Strong consistency. Every participant agrees on the outcome before anything is finalized, matching the guarantee you get from a single database. No window where the system sits in a partially committed state visible to anyone.

Disadvantages: It is a blocking protocol. If the coordinator crashes after collecting votes but before sending the commit decision, every participant is stuck holding its locks indefinitely with no safe way to proceed on its own. A single slow participant holds up the entire transaction, since the coordinator waits for every vote before moving forward. A network partition leaves the coordinator with no safe default, since it cannot tell whether a message got through or not.

This is exactly why almost nobody runs 2PC across independent services in production. The blocking problem is tolerable inside a single distributed database, where the coordinator and participants are tightly coupled parts of the same system with the same failure characteristics. It falls apart across services with different deployment schedules and different failure modes, which is the normal case in a microservices architecture.

The Saga Pattern

The saga pattern starts from a different assumption entirely. Instead of all or nothing atomicity across services, you accept eventual consistency and break the work into a chain of independent local transactions.

1. Charge card commits locally 2. Reserve stock commits locally 3. Record ledger fails, out of stock Release stock compensating action Refund card compensating action Failure triggers compensations in reverse order, not a rollback

Each service does its piece of work and commits to its own database on its own terms. When something fails further down the chain, there is no way to roll back to earlier steps since they are already committed elsewhere. Instead you run a compensating action, a business level undo that reverses the effect of what already happened. A refund instead of a rollback, a cancellation instead of an abort.

Used in: Uber, Netflix, Amazon, and DoorDash all run this pattern in production.

Advantages: Nothing blocks. No coordinator holding locks across services means one slow or failed step does not stall the rest of the system. Other transactions keep flowing normally while a saga is compensating, unlike 2PC where a stuck transaction can hold up unrelated work.

Disadvantages: Some actions are genuinely hard or impossible to undo. You cannot unsend a confirmation email, and a webhook fired to a third party carries no guarantee it gets reversed on request. The system can be temporarily inconsistent while compensations run, and that inconsistency is sometimes visible to the customer, such as a charge appearing before its refund lands. Compensating actions need to be idempotent, since retries are how you handle a compensation that itself fails partway through.

Choreography vs orchestration

There are two ways to implement a saga, and the choice determines who is responsible for detecting failures and running compensations.

Choreography Card Inventory Ledger Event Broker Services publish and react to events. No central control.Orchestration Orchestrator Card Inventory Ledger One service tells each step what to do, in order.

Choreography is decentralized. Each service publishes an event when it finishes its work, and any interested service reacts to that event on its own. This is a natural fit for simple flows of two or three steps where the services are genuinely independent, such as sending a notification after an order is placed. It gets harder to reason about past four or five services, since there is no single place to see the current state of a given transaction.

Orchestration puts a dedicated orchestrator service in charge of the whole flow. It tells each service what to do one step at a time and waits for confirmation before moving on. Its own state is stored durably, so if it crashes it picks up exactly where it left off instead of leaving anything dangling. This is what most teams end up using once they reach any real scale, and tools like Temporal, AWS Step Functions, and Uber’s own Cadence exist specifically for this job.

The dual write problem

One failure mode catches teams off guard even with solid saga logic in place. When a service finishes its work, it typically needs to do two things, save the result to its own database and publish an event so the next step knows to proceed. Those are two separate writes to two separate systems, which is called the dual write problem. If the database write succeeds but the event publish fails, the saga stalls silently. If the publish succeeds but the database write fails, downstream services react to something that never actually happened.

The fix is the transactional outbox pattern. Instead of writing to the database and publishing an event as two separate operations, you write your data and the outgoing event into an outbox table in the same local transaction, so they either both commit or neither does. A separate background process then reads that outbox table and publishes the events to your broker, either by tailing the database’s own change log or by polling the table on an interval.

When you do not need any of this

The first question worth asking before reaching for either pattern is whether you need a distributed transaction at all. If you can design your service boundaries so the data that transacts together lives in the same database, do that. A single local transaction is simpler, faster, and more reliable than any distributed alternative, and this is usually easier to get right up front than to retrofit later. If a particular piece of your system genuinely needs strong consistency, consider whether that data can live in a single distributed database like Spanner or YugabyteDB that handles it internally, which is a very different thing from building 2PC yourself across independent services.

The takeaway

If you genuinely cannot avoid a distributed transaction across services, you are going to use a saga, that part is not really debated anymore. The real question is which flavor fits your situation. Choreography for simple two or three step flows where the services are truly independent and nobody needs a single view of the transaction. Orchestration for anything more complex, branching logic, flows where you need visibility into where a transaction is stuck, or compensation logic complicated enough that you want it defined in one place instead of scattered across a dozen services. Pair either one with a transactional outbox, and accept that eventual consistency is the trade the industry made deliberately, not a compromise it settled for.