The choice between Two-Phase Commit (2PC) and Saga starts with a business rule: what must always hold, and what can be resolved in stages. Preventing a seat from being sold twice requires an atomic decision about inventory. Coordinating that hold with a payment provider also requires resolving effects that cross a system boundary.

Scale affects the cost of coordinating those operations, but the number of services does not determine the protocol on its own. First, establish the transaction boundary and the states the product can accept.

Start with a local transaction

When the data that must change together fits in a single database, a local transaction is often the simplest option. Constraints and conditional transitions can protect the business rule without coordinating a commit across multiple services.

Atomicity guarantees that the changes in that transaction commit together or are rolled back. Isolation determines how concurrent transactions interact; it must be chosen alongside the locks or conditions needed to protect the rule. Using a transaction does not automatically make every sequence of reads and writes serializable.

That transaction does not include an external charge, a sent email, or a call to another service. Even when inventory and the order share a database, coordinating external effects still requires a recovery flow.

What 2PC guarantees

2PC coordinates a commit or abort decision among participants that can prepare their work and retain it durably. It has two phases:

  1. Prepare. The coordinator asks whether each participant can commit. A participant that votes yes persists the state needed to complete the transaction later and may retain locks or other resources.
  2. Decide. If every participant votes yes, the coordinator decides to commit; if it cannot complete preparation, it can decide to abort. It persists the decision before communicating it, and participants apply it to complete the transaction.

The central property is that recoverable participants follow the same decision. 2PC coordinates atomicity; isolation and read guarantees depend on the system implementing it. The protocol alone promises neither linearizable reads nor that all observers see the result at the same instant.

Its difficulty is blocking. If a participant has already prepared and loses contact with the coordinator, it may not know whether to commit or abort. A local timeout does not always allow a safe abort: a commit decision may exist and may have reached other participants.

Work that needs the retained resources may wait until the decision is recovered. A slow participant also prolongs coordination. The impact depends on the affected data, contention, and recovery; a failure does not necessarily stop the entire system.

2PC can fit when all participants support the protocol and the coordination cost is acceptable. A set of arbitrary HTTP APIs, especially an external payment provider, generally does not offer that preparation capability. If a distributed database manages commit internally, its isolation, latency, and failure behavior still need to be reviewed; delegating the implementation does not remove those trade-offs.

What a Saga guarantees

A Saga divides the flow into local transactions. Each step commits its own change and, when needed, has a compensating action. This allows it to coordinate services that do not share a database transaction.

In a purchase, the flow may hold inventory, authorize a payment, and confirm the order. If confirmation becomes impossible, it can void the authorization and release the hold. If the money has already been charged, the corresponding action may be a refund. Each transition validates what happened before and whether it still has the right to modify that state.

A compensation is a new business operation. A refund does not erase the original charge from the history, and a sent email cannot be taken back. Some actions are irreversible; others allow only a partial repair. When the product permits it, steps should be ordered to postpone effects that are difficult to compensate.

A Saga does not provide global isolation either. Other users may observe a pending hold while payment is being resolved, and two flows may compete for the same inventory. Local transactions, hold identifiers, versions, and update conditions must protect those invariants.

Eventual consistency describes a model in which replicas or projections can lag and converge if updates can propagate and new changes stop occurring. In a Saga, resolving the business process additionally requires retries, reconciliation, and compensations that can make progress. Time alone does not repair an incomplete operation.

The flow needs explicit states, such as pending, confirmed, compensating, canceled, or awaiting intervention. A compensation can also fail; it must be recorded so it can be retried or someone can resolve it.

Choreography and orchestration

In choreography, each participant reacts to events and publishes the result of its transaction. This works well when responsibilities are clear and dependencies between events are easy to follow. Traceability must make it possible to reconstruct the flow across services and identify who initiates a compensation.

In orchestration, a component retains the flow’s state, sends commands, and decides which step comes next or what needs compensation. It is useful when there are branches, deadlines, multiple recovery paths, or a clear need to query the complete state of the operation.

There is no technical maximum of three or four steps for choreography. The criterion is how much it costs to understand, change, and recover the flow. Orchestration often makes that work easier as its dependencies grow.

The orchestrator needs durable state and recovery capability. If it restarts, it must know what it requested, which results it received, and what remains pending. A workflow engine can help persist and resume that work, but commands and activities can still repeat; their effects require idempotency.

Transactional outbox

A dual write occurs when a service stores a change in its database and publishes an event to a broker through two independent operations. If it commits the data and crashes before publishing, other services never learn about the change. If it publishes first and the transaction then fails, they may react to a change that never happened.

The outbox closes that gap within the service: the state change and the pending event are written in the same local transaction. If the transaction commits, both are persisted; if it aborts, neither is stored.

An independent process, the relay, reads the outbox through polling or change data capture (CDC) and publishes the events. In an orchestrated Saga, one of those events can communicate the result of a step:

---
config:
  look: handDrawn
---
flowchart TD
    orchestrator["Orchestrator<br/>Durable Saga state"] -->|"Command with identifier"| participant["Participating service"]
    participant --> database[("Local transaction<br/>State + event in outbox")]
    database --> relay["Relay<br/>Polling or CDC"]
    relay -->|"Publish event"| broker["Message broker"]
    broker -->|"Result; may repeat"| orchestrator
    orchestrator -->|"Definitive failure"| compensation["Compensate completed steps<br/>Record and retry"]

The relay can crash after publishing and before recording that it finished. On recovery, it may send the same event again. The outbox makes persistence of the data and pending event atomic; delivery to the broker and subsequent processing can still repeat.

Each event needs a stable identity. The consumer can record the processed event alongside its local change, in the same transaction, to deduplicate deliveries. When ordering matters, versions or sequences per entity are needed. External effects additionally require coordination and idempotency according to their provider’s guarantees.

Outbox lag, failed attempts, and pending messages must be observable. Publication that fails indefinitely needs repair or intervention; storing the event is not enough to get it to its destination.

How to choose

The decision must be tied to a concrete operation. An accounting update may need a strict local transaction, while a transfer to another system requires tracking states and reconciliation. A seat can be assigned through an atomic transition in the inventory database and purchased through a Saga that coordinates the hold, payment, and confirmation.

Aspect2PCSaga
CoordinationCommon commit or abort decision.Sequence of local transactions.
ParticipantsMust support preparation and recovery.Must expose steps and business recovery.
Intermediate statesIsolation determines what can be observed.Committed steps may be visible before the flow finishes.
RecoveryRecover and apply the durable decision.Retry, reconcile, and compensate.
Main costCoupling and retained resources.Complexity of the flow and its compensations.

Start with a local transaction if it can contain the invariant. Evaluate 2PC when you need an atomic decision among compatible participants and accept its operational cost. Use a Saga when the product allows stages and you have defined what to do with each confirmed or uncertain effect.

For complex flows, an orchestrated Saga with idempotent operations and an outbox is often a useful foundation. If a rule cannot tolerate an intermediate state, it must be resolved within an appropriate transaction boundary, or that boundary must be redesigned; adding compensations does not replace that guarantee.

What to verify

For 2PC, tests must stop the coordinator after preparation and before or after persisting the decision. Participant recovery and the time they retain resources must be checked.

For a Saga, it is useful to duplicate commands and events, reverse notification order, and restart the orchestrator between steps. Uncertain outcomes and failing compensations must also be tested. A timeout after requesting a charge requires reconciling its outcome before assuming it did not happen.

For the outbox, the central test stops the relay after publishing and before marking the event as sent. The consumer must tolerate repetition. Useful signals include the time operations remain pending, publication lag, unresolved compensations, and work requiring intervention.

The design connects to a concrete case in SD: Ticketing System, where inventory preserves its local rule while the purchase coordinates payments and recovery.