Skip to main content
Architecture8 min readOctober 8, 2025

The Saga Pattern: Managing Distributed Transactions

When you split a monolith into services, you lose ACID transactions across boundaries. The saga pattern is how you get consistency back.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

The Transaction Problem in Distributed Systems

In a monolithic application with a single database, creating an order is straightforward. You open a transaction, insert the order, decrement inventory, charge the payment, and commit. If any step fails, the transaction rolls back and nothing is half-done. ACID guarantees handle the complexity.

In a distributed system where orders, inventory, and payments are separate services with separate databases, that single transaction does not exist. There is no transaction coordinator that spans three independent databases operated by three independent services. You cannot begin a transaction in the orders database and have it atomically include writes to the inventory and payment databases.

This is not a limitation of any specific technology. It is a fundamental consequence of distributing data across independent stores. Two-phase commit (2PC) protocols exist but are slow, fragile, and create tight coupling between services — exactly what service boundaries are supposed to prevent.

The saga pattern provides an alternative: instead of one atomic transaction, a saga is a sequence of local transactions, each within a single service, coordinated so that the overall business operation either completes successfully or is compensated (undone) if a step fails.


How Sagas Work

A saga decomposes a distributed business operation into a series of steps. Each step is a local transaction within one service. After each step completes, the next step is triggered. If a step fails, compensating transactions are executed for all previously completed steps to undo their effects.

For an order creation saga:

  1. Orders service creates the order in "pending" status (local transaction)
  2. Inventory service reserves the requested items (local transaction)
  3. Payment service charges the customer (local transaction)
  4. Orders service updates the order to "confirmed" (local transaction)

If step 3 fails — the payment is declined — the saga executes compensating actions in reverse:

  1. Inventory service releases the reserved items (compensating transaction)
  2. Orders service updates the order to "cancelled" (compensating transaction)

The result is eventual consistency: there is a brief window where the order exists but is not yet confirmed, and another brief window during compensation where the order is being cancelled but inventory has not yet been released. But the system converges to a consistent state.


Choreography vs. Orchestration

There are two approaches to coordinating the steps:

Choreography uses events. Each service publishes an event when it completes its step, and the next service in the saga listens for that event and performs its step. There is no central coordinator. The saga's logic is distributed across the participating services.

This works well for simple sagas with few steps. Each service is autonomous and reacts to events independently. But as sagas grow in complexity, choreography becomes hard to reason about. The flow of the business operation is implicit in the event subscriptions rather than visible in a single place. Debugging a failed saga requires tracing events across multiple services and their logs.

Orchestration uses a central saga orchestrator that tells each service what to do and when. The orchestrator holds the saga's state machine: which step is current, what happens on success, what happens on failure, which compensating actions to run. Each service exposes command endpoints that the orchestrator calls.

Orchestration is easier to understand and debug because the entire saga flow is defined in one place. The trade-off is that the orchestrator becomes a single point of coordination — though not a single point of failure if implemented with durable state and retry logic.

For most production systems I build, I prefer orchestration for anything beyond two or three steps. The visibility and debuggability are worth the additional component. The orchestrator is typically a lightweight service that manages saga state in its own database and communicates with participants through asynchronous messaging.


Designing Compensating Actions

The hardest part of implementing sagas is designing compensating transactions. Not every action has an obvious undo.

Reversible actions are straightforward: if you reserved inventory, release it. If you created a pending order, cancel it. The compensating action is a logical inverse.

Non-reversible actions require creative compensation. If you sent a confirmation email, you cannot unsend it — but you can send a cancellation email. If you charged a payment, the compensating action is a refund rather than a reversal (and refunds have their own failure modes). If you called a third-party API that triggered an irreversible side effect, the compensation might involve creating a manual remediation task.

A few principles help:

Design services to support compensation from the start. If a service creates a resource, it should support a "cancel" or "undo" operation. Bolting compensation onto a service that was not designed for it is painful.

Use status fields rather than deletes. An order that moves through "pending," "confirmed," and "cancelled" states preserves history and makes compensation visible. Deleting the order row as a compensating action loses the audit trail.

Make compensating actions idempotent. Network failures mean compensating actions might be delivered more than once. If releasing inventory is called twice, the second call should be a no-op rather than releasing additional items.

The event-driven architecture that supports sagas also supports observability. Publishing events for each saga step and compensation creates an audit log that makes debugging failed sagas tractable.


Sagas are not a drop-in replacement for ACID transactions. They are more complex to implement, harder to reason about, and introduce eventual consistency that the rest of the system must tolerate. But when your architecture genuinely requires distributed data ownership, sagas are the proven pattern for maintaining business consistency without sacrificing service independence.


If you are building a distributed system and need help designing saga flows that handle real-world failure modes, let's talk.


Keep Reading