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.
The Mapping
Section titled “The Mapping”| DDD Concept | Ironflow Implementation |
|---|---|
| Saga step | step.run("name", fn) |
| Compensating action | step.compensate("name", undoFn) |
| Saga coordinator / orchestrator | The function itself |
| Terminal failure (triggers compensation) | throw new NonRetryableError(...) |
| Compensation durability | Memoized as compensate:step-name steps |
Order Fulfillment Saga
Section titled “Order Fulfillment Saga”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:
reserve-inventorycompensation (releases inventory, undoes step 2)charge-paymentcompensation (refunds payment, undoes step 1)
Orchestration, Not Choreography
Section titled “Orchestration, Not Choreography”In DDD literature, sagas come in two flavors. Ironflow’s model is orchestrated — the function is the coordinator:
| Style | How it works | Ironflow? |
|---|---|---|
| Choreography | Each step publishes events that trigger the next step. No central control. | Not directly — but you can chain functions via event triggers for this pattern. |
| Orchestration | A 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.
Sagas vs Process Managers
Section titled “Sagas vs Process Managers”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 resultsconst 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 () => { ... });}Key Takeaways
Section titled “Key Takeaways”- 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.
NonRetryableErrortriggers compensation — transient errors retry, terminal errors compensate.
For the full saga API (compensation ordering, parallel branches, failure handling), see the Sagas & Compensation guide.