The saga pattern is a design pattern for coordinating that kind of operation without a distributed transaction. It breaks the work into local transactions that each commit on their own, and pairs every one with a compensating action that reverses it when a later step fails. Isolation is the one guarantee it can't offer.
This article covers how sagas are coordinated, what that missing isolation costs in production, and when a shared database removes the need for the pattern altogether.
Key takeaways:
The saga pattern breaks one business transaction into a series of local ones, giving each a compensating action that undoes it when a later step fails.
Sagas give you atomicity, consistency, and durability, but not isolation, so anything reading concurrently can see a saga mid-flight.
Two-phase commit stalls every participant when its coordinator crashes, and plenty of managed databases and brokers don't support it at all.
Choreography-based sagas need a transactional outbox, because a crash between the local commit and the event publish strands the saga with nothing to recover it.
Most teams reach for sagas because of a service split they didn't need, and a shared database with ordinary transactions makes the whole pattern go away.
Copy link to headingWhat is the saga pattern?
The saga pattern is a way of running a long-lived business transaction as a sequence of local database transactions, each committing independently and each paired with a compensating transaction that reverses it. Formalized by Garcia-Molina and Salem in a 1987 paper, it guarantees one of two outcomes. Either every step completes, or the steps that have already run are undone in reverse.
The pattern exists because some operations can't fit inside one database transaction. Placing an order touches order creation, payment capture, inventory reservation, and shipment scheduling. When each of those is owned by a different service with its own database, no single transaction spans them, and consistency moves out of the database and into your application code.
Copy link to headingHow the saga pattern differs from two-phase commit
The two approaches split on what happens when something crashes halfway through. Two-phase commit (2PC) is a protocol for committing one transaction across multiple databases at once, all or nothing.
A coordinator collects YES votes from every participant, then broadcasts a commit or abort decision. If the coordinator crashes between those two moments, every participant is stuck holding locks, unable to commit or abort on its own because either choice might contradict the decision it never received. They wait for a coordinator that may never come back.
A saga has no equivalent moment to get stuck in, because every step has already been committed by the time the next one starts.
The two approaches diverge on what they're willing to give up under partial failure:
The last row is the one that decides real architectures. Two-phase commit only works if every participant speaks XA, the standard interface that lets an outside transaction manager drive a commit across separate systems. Managed cloud services largely don't implement it.
Aurora PostgreSQL write forwarding lists PREPARE TRANSACTION, COMMIT PREPARED, and ROLLBACK PREPARED as unsupported. DynamoDB transactions can't reach past a single AWS account and region. Kafka keeps its exactly-once guarantees inside its own transactional API, so your database can't join a Kafka transaction.
Once an operation spans two vendors, 2PC isn't an option you weighed and rejected. It was never available, and a saga is what's left.
Copy link to headingWhy the saga pattern trades isolation for availability
Isolation is the guarantee a saga gives up. A database transaction normally hides its in-progress state from everyone else until it commits or rolls back. A saga can't do that, because it's really a chain of separate transactions, each one visible to the rest of the system the moment it commits.
Anything else reading that data mid-saga can see a step's result before the saga finishes, and act on it before a later compensation has a chance to undo it.
That produces three anomalies your application code now has to handle:
Lost updates: Two sagas write the same record, and when the first one compensates, it restores the old value and erases what the second one wrote.
Dirty reads: One saga reads a record another saga has written but not yet finished, so the value it acts on may be reversed moments later.
Non-repeatable reads: A saga reads a value, makes a decision on it, then finds that the value has changed by the time a later step runs.
Compensation is also weaker than it sounds. A database rollback restores the prior state atomically and leaves no trace, while a compensation approximates that state with a new forward write. The original operation stays in the audit log alongside its reversal.
For a refund, that's correct behavior. The customer doesn't care that the charge and the reversal both exist in the ledger. For something the customer directly experienced, like a confirmation email or a status change they already acted on, a forward-only undo isn't enough on its own. Plan for that at design time, not after it shows up in an incident.
Copy link to headingCore components of a saga pattern implementation
Every saga implementation, whatever framework runs it, assembles the same five parts.
Copy link to headingLocal transactions, the pivot, and retryable steps
Not every step in a saga carries the same recovery options. Steps fall into three categories, and where a step sits in the sequence determines what you can still undo. Compensable transactions can be semantically undone.
The pivot transaction is the point of no return, and once it commits, compensation is off the table. Retryable transactions follow the pivot and must be idempotent, because the only remaining path is forward until they succeed.
Ordering steps by category is a design decision with real consequences. Placing an irreversible external call late in the sequence keeps compensation available for as long as possible. Placing it early means most of the saga has no way back.
Copy link to headingCompensating transactions
A compensating transaction is a new operation that semantically reverses a committed step. Releasing a reserved inventory hold, refunding a captured payment, and returning consumed credits are all compensations.
They carry two requirements that are short to state and routinely missed in implementation. Compensations must be idempotent, because the recovery path can run more than once. And a compensation should only be registered after its forward step succeeds, since compensating work that never happened produces its own inconsistency.
Compensations belong in durable units with their own retry behavior rather than in a catch block as cleanup code. A compensation that fails silently leaves the system in exactly the state the saga was meant to prevent.
Copy link to headingOrchestration and choreography as coordination models
Sagas are coordinated one of two ways. With orchestration, a central coordinator issues commands and tracks saga state. With choreography, each service commits locally, publishes an event, and downstream services react.
The choice determines where your debugging pain lands:
Orchestration is the more common production choice. Under choreography, reconstructing why a saga stalled means correlating logs across every participant, and that cost grows with each service added to the flow.
Copy link to headingThe transactional outbox
One structural constraint sits under every choreography-based saga. Each step must publish its event atomically with its local database commit. If the process crashes after the commit but before the publish, the event is lost and the saga stalls with no automatic recovery.
A transactional outbox closes that window by writing the outgoing event into the same local transaction as the business data, with a separate relay publishing it to the broker afterward. Treating the outbox as optional gives a choreographed saga a reliability hole it can't self-heal.
Event sourcing is the structural alternative, since an append-only log makes every state change publishable and removes the dual write at the storage layer. It's also a major architectural commitment that changes how state is stored, queried, and evolved, so it earns its place mainly when the event log has first-class product value.
Copy link to headingIdempotent consumers
Every message broker worth using delivers at least once, which means every saga step handler will eventually receive a duplicate. Handlers that increment counters, capture payments, or send notifications produce visible damage when they run twice.
Deduplication has to live outside the handler. In-memory maps only hold within a single process instance, so on any platform where instances come and go, the deduplication store has to outlive the function, and a message that never deduplicates cleanly needs a quarantine destination rather than an unbounded retry. Designing around at-least-once delivery starts with a working grasp of API idempotency.
Copy link to headingHow Vercel Workflows runs the saga pattern without an orchestrator
Vercel Workflows provides the durable execution layer a saga needs, with the coordination expressed in application code rather than a separate orchestration service. Marking a function with "use workflow" makes it durable, and marking each unit of work with "use step" checkpoints makes it automatic.
Workflows reached general availability in April 2026, and since the October 2025 beta, it has processed over 100 million runs and over 500 million steps across more than 1,500 teams.
Copy link to headingDurable execution replaces the coordinator you'd otherwise operate
Orchestration solves the traceability problem and introduces an availability one, because the coordinator is a long-lived process you now run, scale, and keep healthy. Building your own usually means maintaining a worker fleet, a status table, and retry logic as a distributed system sitting on top of the application it coordinates.
Vercel Workflows remove the separate orchestrator. Each step runs as its own function invocation, Vercel Queues enqueues the next one reliably, and managed persistence stores the event log that records every step input, output, and error. If a process crashes mid-run, the log replays and execution resumes from the last completed step rather than starting over.
Determinism is the constraint that makes replay safe, so the SDK intercepts Math.random() and Date.now() and keeps workflow functions sandboxed from the network and filesystem. That sandbox is a real restriction on how orchestration code gets written, and existing coordination logic that reads a clock or calls out mid-flow has to move into steps before it will run at all.
Copy link to headingCompensating transactions as durable rollback steps
Compensation logic written as inline cleanup in a catch block inherits none of the durability of the steps it's undoing. If the process dies during rollback, the partial compensation is lost, and the saga ends in a state neither committed nor reversed.
The documented rollback pattern treats compensations as steps in their own right. Each one then retries and persists like any forward step:
// Forward stepsasync function reserveInventory(orderId: string) { "use step"; // ... call inventory service to reserve ...}
async function chargePayment(orderId: string) { "use step"; // ... charge the customer ...}
// Rollback stepsasync function releaseInventory(orderId: string) { "use step"; // ... undo inventory reservation ...}
async function refundPayment(orderId: string) { "use step"; // ... refund the charge ...}
export async function placeOrderSaga(orderId: string) { "use workflow";
const rollbacks: Array<() => Promise<void>> = [];
try { await reserveInventory(orderId); rollbacks.push(() => releaseInventory(orderId));
await chargePayment(orderId); rollbacks.push(() => refundPayment(orderId));
// ... more steps & rollbacks ... } catch (e) { for (const rollback of rollbacks.reverse()) { await rollback(); } // Rethrow so the workflow records the failure after rollbacks throw e; }}Each compensation registers only after its forward step succeeds, and the reversed iteration runs them in the order a saga requires. Flora uses Vercel Workflows to orchestrate more than 50 image models, with steps that persist and retry on failure.
Copy link to headingRetries and failure classification at the step boundary
The distinction between compensable and retryable transactions only matters if your runtime can act on it. Uniform retry behavior forces the choice into application code, where a transient rate limit and a permanent validation error get handled by the same branch.
Vercel Workflows classifies failures at the step boundary. Steps retry up to 3 times by default, for 4 total attempts, and maxRetries adjusts that per step. Throwing FatalError skips retries entirely, which is the correct response to a 404 or a rejected payload, while throwing RetryableError with a retryAfter value sets the delay for a rate-limited downstream service.
getStepMetadata() exposes the current attempt number on top of that, so a retryable step past the pivot can apply exponential backoff without you writing a scheduler.
Copy link to headingIdempotency keys derived from a stable step identity
Retries reintroduce the duplicate-side-effect problem that a saga was supposed to contain. A payment step that retries after a lost confirmation response charges the customer twice, and no amount of compensation logic fixes a charge you didn't know happened.
Every step invocation has a stepId that stays the same across retries and is globally unique. That makes it a correct idempotency key for third-party calls:
import { getStepMetadata } from "workflow";
async function chargeUser(userId: string, amount: number) { "use step"; const { stepId } = getStepMetadata();
await stripe.charges.create( { amount, currency: "usd", customer: userId, }, { idempotencyKey: stepId, } );}The same identifier works for compensations, which need idempotency for the same reason. Deriving the key from a timestamp or a random value defeats the purpose, because a retry then presents a key the downstream API has never seen.
Copy link to headingAt-least-once delivery without an outbox to operate
Choreographed sagas carry the outbox as permanent infrastructure, including a relay process, a polling interval, and a table that needs its own monitoring. All of it exists to close one crash window between a local commit and an event publish.
Under orchestration on Workflows, that window closes differently. Step results and the continuation to the next step both land in the same managed event log, so there's no separate publish that can drift from the recorded state.
Vercel Queues handles delivery underneath with at-least-once semantics, retries, and visibility timeouts, and an idempotency key on publish deduplicates a retried send for the lifetime of the original message.
This doesn't eliminate every dual write. A step that writes to your own database and then crashes before reporting its result will re-execute, which is why steps still need to be idempotent. What it removes is the broker-publish gap and the infrastructure you'd otherwise run to cover it.
Copy link to headingWhen to use the saga pattern instead of a shared database
A saga makes sense once an architecture has already committed to database-per-service and the coordinated operations cross a vendor boundary where 2PC isn't available. Most teams never reach that point, so the first question is which side of it you're on.
Copy link to headingMost saga adoptions trace back to a split that wasn't needed
The saga is usually a symptom of an earlier decision, a service split the team didn't need at its scale. Reverse that split back into a shared database and the saga has nothing left to coordinate, because ACID transactions, meaning atomic, consistent, isolated, and durable, handle the consistency the saga was built to fake.
The compensation logic, the isolation anomalies, and the outbox all go with it. For a team of five to fifty engineers, that consolidation is usually the right call.
Copy link to headingWhen coordination genuinely can't be collapsed
Some splits are real, and no shared database can absorb them. A payment flow that crosses a payment processor, a fraud service, and your own order database has three separate systems by necessity, not by choice.
Media generation pipelines and AI agent workflows are the same, calling third-party services that can't join any transaction you control, where a mid-run failure may demand a refund or a quota return. When the boundary is genuinely external, the saga is earned, and the only remaining question is which layer runs it.
Copy link to headingRun the saga pattern on durable infrastructure
The order that commits while the payment times out is a real problem, and compensating transactions are a real answer to it. They're also a permanent tax on every operation that touches the flow, paid in isolation anomalies, countermeasures, and recovery code that only runs on the worst day.
Getting this right starts with checking whether the services involved need separate databases at all. When the answer is yes, the coordination machinery belongs in the platform rather than in a second system you maintain.
Here's what Vercel provides for teams running coordinated operations across service and vendor boundaries:
Vercel Workflows: Durable execution with automatic step checkpointing through the
"use workflow"and"use step"directives, so replay resumes from the last completed step after a crash rather than re-running the saga from the beginning.Durable rollback steps: Compensations written as steps get the same persistence and retry behavior as forward work, which keeps recovery from failing silently at the moment it matters most.
Step-level failure classification:
FatalError,RetryableErrorwith a configurableretryAfter, and per-stepmaxRetrieslet compensable and retryable transactions behave differently without custom branching.Stable step identity:
getStepMetadata()returns astepIdthat survives retries, giving every external call and compensation a correct idempotency key.Vercel Queues: At-least-once delivery with retries, visibility timeouts, and publish-time deduplication, running underneath workflows without a broker or relay to operate.
Start a new project and ship a durable workflow on your first git push, or adapt a multi-step template to your own services.
Copy link to headingFrequently asked questions about the saga pattern
Copy link to headingWhat is the difference between saga orchestration and choreography?
Orchestration uses a central coordinator that tracks saga state and issues commands to each participant. Choreography distributes coordination through events, with each service reacting to the previous step's output. Orchestration gives clearer visibility into the global state, while choreography avoids a single point of failure at the cost of harder tracing.
Copy link to headingWhat is a compensating transaction?
A compensating transaction reverses the business effect of a step that has already been committed, such as refunding a captured payment or releasing an inventory hold. It runs as a new write rather than a rollback, so it must be idempotent and can only be registered once its forward step has succeeded.
Copy link to headingCan you implement the saga pattern on a serverless platform like Vercel?
Yes. Vercel Workflows supplies the durable execution layer with step isolation, automatic retries, deterministic replay after a crash, and sleep that holds no compute. The "use workflow" and "use step" directives cover the infrastructure, while application code still owns the compensation logic and business rules.
Copy link to headingWhen should you use a modular monolith instead of the saga pattern?
A modular monolith fits when the services involved would share a database anyway, since ACID transactions remove the need for compensation entirely. The same holds when a team is small enough that service boundaries add more coordination overhead than they remove.