Saga Pattern for Multi-Step Workflows

A workflow reserves inventory, charges a payment, and schedules delivery. Each step is handled by a different component with its own state. The inventory reservation succeeds, but payment fails. What should the system do with the reservation that already committed?

A single database transaction can’t usually roll back work that has already been committed by independent components. The saga pattern handles this kind of workflow by treating it as a sequence of local transactions. When a later step fails, the workflow runs explicit compensating actions for earlier steps where business reversal is possible.

The key idea is not “distributed rollback.” A saga accepts that intermediate changes can become visible and then manages the consequences deliberately. This article explains that mental model, how compensation differs from undo, and how to decide whether a saga is appropriate.

A saga manages committed steps, not one large transaction

Start with a three-step order workflow:

1. reserve inventory
2. charge payment
3. schedule delivery

If all three changes lived in one transactional database, the application might put them inside one transaction and commit only when every step succeeds. A failure before commit could roll the whole transaction back.

Now suppose each step belongs to an independent service or subsystem. The inventory service commits its reservation before the payment service is called. By the time payment fails, the inventory change is already durable.

The useful mental model is:

local commit -> local commit -> local commit
     |              |
     +------ workflow coordination ------+

Each participant owns its own transaction. The saga coordinates the sequence and records enough progress to decide what should happen next.

This changes the failure question. Instead of asking, “How do we roll back the transaction?” ask, “Given the steps that have already committed, what business action restores an acceptable state?”

Compensation is a new action, not time travel

Suppose inventory reservation succeeds and payment fails. A compensation might release the reservation:

reserve inventory  -> success
charge payment     -> failure
release inventory  -> compensation

release inventory is not a database rollback of the original reservation transaction. It is another operation with its own transaction, failure modes, logs, and observable effects.

That distinction matters because the world may have changed between the original action and its compensation. If the workflow charged a card and later issues a refund, the refund doesn’t erase the charge from history. Both actions happened. Fees, notifications, audit records, or external processing may still exist.

A good compensating action therefore restores a business invariant rather than pretending the earlier action never occurred. For an inventory reservation, the invariant might be “stock reserved for an abandoned order becomes available again.” For a payment, it might be “the customer does not remain charged for an order that cannot be fulfilled.”

Some actions have no meaningful compensation. Sending an email cannot be unsent. Publishing information to an external system may be irreversible. In those cases, ordering matters: delay irreversible actions until the workflow has passed the steps most likely to require compensation, when the business process allows it.

Design the forward path and compensation together

A saga is easier to reason about when every reversible forward step has an explicit compensating action.

For example:

Forward action          Compensation
--------------          ------------
reserve inventory       release reservation
authorize payment       void authorization
create shipment         cancel shipment

This table is deliberately about business operations, not implementation calls. A production workflow needs to define what each action means and under which states it remains valid.

Consider payment authorization. Voiding an authorization may be a valid compensation before settlement, while a settled payment may require a refund instead. The compensation therefore depends on the state reached by the forward action. “Call the opposite API” is not a sufficient design rule.

The workflow also needs to know which steps actually succeeded. If payment times out, the coordinator may not know whether the payment provider committed the charge before the response was lost. Blindly assuming failure can produce a duplicate charge on retry; blindly compensating can reverse a payment that the workflow still intends to keep.

For ambiguous outcomes, design operations so the coordinator can safely retry or query their status. Stable operation identifiers and idempotent handling are common tools for this, but they don’t remove the need to model the uncertain state explicitly.

Compensation can fail too

The happy compensation path is easy to draw:

A succeeds
B succeeds
C fails
compensate B
compensate A

Real systems have another failure path:

C fails
compensate B -> succeeds
compensate A -> times out

The saga is now incomplete. Treating compensation as infallible would leave the system stuck while reporting that recovery succeeded.

A robust saga keeps durable workflow state so recovery can continue after process crashes and transient failures. The state might record that A and B completed, C failed, B was compensated, and compensation for A is still pending. A worker can retry the pending action according to the operation’s retry policy.

Retries require care. If a compensation can be submitted more than once, the receiving component should either make repeated requests safe or use a stable request identity to recognize duplicates. “Exactly once” should not be assumed merely because the coordinator sent one logical command.

Some failures won’t resolve automatically. A refund can be rejected. A reservation may already have expired. An external provider can remain unavailable beyond the workflow’s acceptable recovery window. The design needs an explicit terminal or manual-review state for cases where automated compensation cannot restore the intended invariant.

Choose how the saga is coordinated

There are two common coordination styles.

With orchestration, one coordinator decides which step runs next. It receives outcomes, records progress, and issues forward or compensating commands.

             +-> inventory
coordinator -+-> payment
             +-> shipping

This makes the workflow order visible in one place. It is often easier to inspect when the process has branching rules, several compensations, or operational states such as payment_pending and compensation_failed. The trade-off is that the coordinator becomes an important component whose responsibilities must stay focused on workflow policy rather than absorbing each participant’s business logic.

With choreography, participants react to events and emit events that trigger later work. There may be no single component that contains the complete sequence.

OrderCreated
    -> InventoryReserved
        -> PaymentAuthorized
            -> ShipmentScheduled

Choreography can fit short workflows whose event relationships are already natural. As the number of branches and compensations grows, though, reconstructing “what happens next?” may require reading several handlers across different components. At that point an explicit orchestrator can make the process easier to understand and operate.

Neither style changes the fundamental saga semantics: local commits remain local commits, and recovery is performed through later business actions.

Make intermediate states part of the design

Because a saga commits incrementally, other parts of the system may observe intermediate state. An order might be payment_pending while inventory is already reserved. A customer query can arrive during that window.

Don’t hide this with a boolean such as complete = false if callers need to make different decisions for different states. Model the workflow states that affect behavior:

created
inventory_reserved
payment_authorized
shipping_scheduled
compensating
cancelled
manual_review

The exact states depend on the domain. The point is to make meaningful intermediate conditions explicit enough that readers, retries, monitoring, and support tooling can distinguish them.

This also helps define invariants. For example, an order in cancelled must have no active inventory reservation and no unreversed customer charge. An order in manual_review may temporarily violate that target condition, but the system should surface that fact rather than presenting the order as cleanly cancelled.

Common saga mistakes

One mistake is treating compensation as a technical inverse. deleteOrder() is not necessarily the correct opposite of createOrder(). If downstream processes have already observed the order, deletion may destroy information needed for audit or recovery. A state transition such as cancelled can preserve the history while preventing further fulfillment.

Another mistake is compensating every earlier step automatically without checking whether reversal is still valid. Business actions have boundaries. A shipment that has already left a warehouse may require a return workflow rather than cancelShipment.

A third mistake is keeping workflow progress only in memory. A coordinator that crashes after a participant commits but before progress is recorded can lose the information needed to continue safely. The coordinator’s durable state and participant operation identities are part of the correctness design, not merely operational extras.

Finally, avoid building a saga when the work can remain inside one ordinary transaction. A saga introduces intermediate states, compensation logic, retry behavior, and more complicated observability. If several changes belong to the same transactional boundary and can commit atomically, the simpler local transaction usually gives stronger semantics with less code.

When a saga is the right trade-off

A saga is useful when one business workflow spans independent transactional boundaries, partial progress can occur, and the domain has meaningful ways to compensate or otherwise resolve that progress.

It is a poor fit when the business requires strict all-or-nothing visibility across every participant and temporary inconsistency is unacceptable. Compensation cannot provide the same isolation as a single atomic transaction. Other coordination mechanisms or a different system boundary may be required.

It is also a warning sign when most steps are irreversible and failure after each one needs extensive manual repair. Calling the process a saga doesn’t solve that mismatch. Reordering work, changing where the transactional boundary sits, or redesigning the business process may reduce the number of dangerous intermediate states.

Start with the failure table

Before implementing a multi-step workflow, write down each forward action, what durable state it creates, what failures are ambiguous, and what compensation is valid after it succeeds. Then walk through failure after every step.

That exercise exposes the real design earlier than a sequence diagram showing only the success path. A saga works when the team can explain not just how the workflow moves forward, but how it reaches an acceptable state after any committed step fails to lead to the next one.