Skip to content

Sagas & Process Managers

Ironflow’s saga pattern coordinates multi-step processes across aggregate boundaries with automatic compensation. Every durable workflow is a potential saga — just register undo actions for your side effects.

DDD ConceptIronflow Implementation
Saga stepstep.run("name", fn)
Compensating actionstep.compensate("name", undoFn)
Saga coordinator / orchestratorThe function itself
Terminal failure (triggers compensation)throw new NonRetryableError(...)
Compensation durabilityMemoized as compensate:step-name steps

The classic DDD example — an order that spans payment, inventory, and shipping:

import { ironflow } from "@ironflow/node";
import { NonRetryableError } from "@ironflow/core";
export const fulfillOrder = ironflow.createFunction(
{
id: "fulfill-order",
triggers: [{ event: "order.placed" }],
},
async ({ event, step }) => {
// Step 1: Charge payment
const payment = await step.run("charge-payment", async () => {
return await stripe.charges.create({
amount: event.data.total,
customer: event.data.customerId,
});
});
// Register undo: refund if later steps fail
step.compensate("charge-payment", async () => {
await stripe.refunds.create({ charge: payment.id });
});
// Step 2: Reserve inventory
const reservation = await step.run("reserve-inventory", async () => {
return await inventory.reserve(event.data.items);
});
// Register undo: release inventory if shipping fails
step.compensate("reserve-inventory", async () => {
await inventory.release(reservation.id);
});
// Step 3: Create shipment — if this fails terminally,
// inventory is released and payment is refunded automatically
await step.run("create-shipment", async () => {
const result = await shipping.createLabel(event.data.address);
if (!result.ok) {
throw new NonRetryableError("Shipping unavailable");
}
return result;
});
},
);

If create-shipment throws a NonRetryableError, Ironflow automatically runs compensations in reverse:

  1. reserve-inventory compensation (releases inventory, undoes step 2)
  2. charge-payment compensation (refunds payment, undoes step 1)

In DDD literature, sagas come in two flavors. Ironflow’s model is orchestrated — the function is the coordinator:

StyleHow it worksIronflow?
ChoreographyEach step publishes events that trigger the next step. No central control.Not directly — but you can chain functions via event triggers for this pattern.
OrchestrationA coordinator directs each step in sequence.Yes — step.run() calls are the orchestrated sequence.

The orchestration approach is simpler to reason about: all the saga logic lives in one function, the execution order is explicit, and compensations are registered right next to the steps they undo.

DDD distinguishes between sagas and process managers:

  • Saga — a fixed sequence of steps with compensations. The flow is predetermined.
  • Process Manager — a stateful coordinator that can make decisions based on events. The flow adapts based on what happens.

Ironflow workflows support both patterns. A simple step.run()step.compensate() chain is a saga. A workflow with conditional logic (if/else based on step results) is a process manager:

// Process manager pattern — flow adapts based on results
const risk = await step.run("assess-risk", async () => {
return await riskService.evaluate(event.data);
});
if (risk.score > 80) {
// High risk: manual review path
await step.run("flag-for-review", async () => { ... });
} else {
// Low risk: auto-approve path
await step.run("auto-approve", async () => { ... });
}
  • Every workflow is a potential saga — add step.compensate() to get automatic rollback.
  • Ironflow uses orchestration — the function coordinates the steps, not events.
  • Compensations are durable — memoized and exactly-once, even across crashes.
  • NonRetryableError triggers compensation — transient errors retry, terminal errors compensate.

For the full saga API (compensation ordering, parallel branches, failure handling), see the Sagas & Compensation guide.