<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[sayiir.dev — Rust-powered workflow engine for people who just want to ship]]></title><description><![CDATA[sayiir.dev — Rust-powered workflow engine for people who just want to ship]]></description><link>https://sayiir.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 05 Sep 2026 11:14:41 GMT</lastBuildDate><atom:link href="https://sayiir.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to build crash-proof order pipelines with durable workflow]]></title><description><![CDATA[You Charged the Customer. Then Shipping Went Down. Now What?
You've seen it before. A customer places an order. Your code charges their card, reserves inventory, calls the shipping API — and the shipp]]></description><link>https://sayiir.hashnode.dev/how-to-build-crash-proof-order-pipelines-with-durable-workflow</link><guid isPermaLink="true">https://sayiir.hashnode.dev/how-to-build-crash-proof-order-pipelines-with-durable-workflow</guid><category><![CDATA[Workflow Automation]]></category><category><![CDATA[temporalio]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Backend Development]]></category><category><![CDATA[ecommerce]]></category><dc:creator><![CDATA[Yacine]]></dc:creator><pubDate>Mon, 23 Feb 2026 20:58:25 GMT</pubDate><content:encoded><![CDATA[<p><strong>You Charged the Customer. Then Shipping Went Down. Now What?</strong></p>
<p>You've seen it before. A customer places an order. Your code charges their card, reserves inventory, calls the shipping API — and the shipping API times out. The payment went through. The inventory is locked. But the order is stuck in limbo.</p>
<p>Now you're writing compensating transactions at 2 AM, praying your retry logic doesn't double-charge anyone.</p>
<p>There's a better way.</p>
<h2><strong>The problem: partial failure</strong></h2>
<p>Here's what a typical order pipeline looks like:</p>
<pre><code class="language-typescript">async function processOrder(order) {
  await validateOrder(order);
  await chargePayment(order);          // ✓ $79.99 charged
  await reserveInventory(order);       // ✓ inventory locked
  await arrangeShipping(order);        // 💥 shipping API timeout
  await sendConfirmation(order);       // never reached
}
</code></pre>
<p>If <code>arrangeShipping</code> fails, you have a real problem:</p>
<ul>
<li><p>The customer is charged</p>
</li>
<li><p>Inventory is reserved</p>
</li>
<li><p>But the order isn't shipped</p>
</li>
<li><p>And if you retry from the top, you'll double-charge them</p>
</li>
</ul>
<p>You could wrap each step in try/catch and write rollback logic. You could add idempotency keys everywhere. You could build a state machine. All of these work — and all of them are tedious, error-prone, and hard to test.</p>
<h2><strong>The pattern: checkpoint and resume</strong></h2>
<p>The idea is simple: <strong>checkpoint each step's output as it completes</strong>. When you resume after a crash, skip the steps that already succeeded and pick up where you left off.</p>
<p>This is essentially what saga orchestrators do, but instead of building the infrastructure yourself, you define the pipeline declaratively and let the engine handle checkpointing, retries, and resumption.</p>
<h2><strong>Building it with sayiir</strong></h2>
<p><a href="https://github.com/sayiir-org/sayiir">Sayiir</a> is a durable workflow engine with a Rust core and bindings for Node.js and Python. Let's build our order saga.</p>
<h3><strong>Step 1: Define the tasks</strong></h3>
<p>Each step in the pipeline is a <code>task</code> — a named, checkpointed unit of work:</p>
<pre><code class="language-typescript">import {
  task, flow, branch,
  runDurableWorkflow, resumeWorkflow, InMemoryBackend,
} from "sayiir";

const validateOrder = task("validate-order", (order) =&gt; {
  return { ...order, validated: true };
});

const chargePayment = task("charge-payment", (order) =&gt; {
  return { paymentId: `pay_${order.id}`, amount: order.amount };
}, { timeout: "30s", retry: { maxAttempts: 3, initialDelay: "1s", backoffMultiplier: 2.0 } });

const reserveInventory = task("reserve-inventory", (order) =&gt; {
  return { item: order.item, reserved: true };
});

const arrangeShipping = task("arrange-shipping", ([payment, inventory]) =&gt; {
  const trackingId = "TRACK-" + Math.random().toString(36).slice(2, 8).toUpperCase();
  return { paymentId: payment.paymentId, item: inventory.item, trackingId };
});

const sendConfirmation = task("send-confirmation", (result) =&gt; {
  return `Order complete! \({result.item} ships via \){result.trackingId}`;
});
</code></pre>
<p>Each task has a unique string ID (<code>"validate-order"</code>, <code>"charge-payment"</code>, …). That's how the engine knows which checkpoint belongs to which step. Notice <code>chargePayment</code> has retry and timeout config built in — no wrapper code needed.</p>
<h3><strong>Step 2: Compose the workflow</strong></h3>
<pre><code class="language-typescript">const workflow = flow("order-saga")
  .then(validateOrder)
  .fork([
    branch("payment", chargePayment),
    branch("inventory", reserveInventory),
  ])
  .join("merge", arrangeShipping)
  .then(sendConfirmation)
  .build();
</code></pre>
<p>This reads top-to-bottom: validate, then fork into parallel payment + inventory, join the results into shipping, then confirm. The <code>fork</code>/<code>join</code> runs payment and inventory concurrently — both must complete before shipping starts.</p>
<h3><strong>Step 3: Run it with a backend</strong></h3>
<pre><code class="language-typescript">const backend = new InMemoryBackend();
// For production: const backend = PostgresBackend.connect(process.env.DATABASE_URL);

const order = { id: "ORD-42", item: "Wireless Keyboard", amount: 79.99 };
const status = runDurableWorkflow(workflow, `saga-${order.id}`, order, backend);
</code></pre>
<p>Every task's output is checkpointed to the backend. If the process dies, the checkpoints survive (use <code>PostgresBackend</code> in production).</p>
<h2><strong>What happens on crash</strong></h2>
<p>Now let's make it interesting. What if the shipping API is down when we run?</p>
<p>We can simulate this with a flag that makes <code>arrangeShipping</code> throw:</p>
<pre><code class="language-typescript">let shippingDown = true;

const arrangeShipping = task("arrange-shipping", ([payment, inventory]) =&gt; {
  if (shippingDown) throw new Error("Shipping API is down!");
  const trackingId = "TRACK-" + Math.random().toString(36).slice(2, 8).toUpperCase();
  return { paymentId: payment.paymentId, item: inventory.item, trackingId };
});
</code></pre>
<p>Run 1 — shipping blows up after payment and inventory succeed:</p>
<pre><code class="language-plaintext">  [validate-order]      ✓ checkpointed
  [charge-payment]      ✓ checkpointed
  [reserve-inventory]   ✓ checkpointed
  [arrange-shipping]    ✗ Error: Shipping API is down!

Status: failed
</code></pre>
<p>Payment went through. Inventory is reserved. The workflow is stuck at the shipping step. But here's the key — <strong>all three completed steps are checkpointed</strong>.</p>
<p>Now the shipping API recovers. We flip the flag and resume:</p>
<pre><code class="language-typescript">shippingDown = false;
const run2 = resumeWorkflow(workflow, "saga-ORD-42", backend);
</code></pre>
<pre><code class="language-plaintext">  [validate-order]      ⟳ skipped (cached)
  [charge-payment]      ⟳ skipped (cached)
  [reserve-inventory]   ⟳ skipped (cached)
  [arrange-shipping]    ✓ checkpointed
  [send-confirmation]   ✓ checkpointed

Status: completed
Result: Order complete! Wireless Keyboard ships via TRACK-A1B2C3
</code></pre>
<p>Validate, charge, and inventory were <strong>skipped</strong> — their outputs were replayed from checkpoints. Only shipping and confirmation actually ran. No double-charges. No re-reservations. The workflow picked up exactly where it left off.</p>
<h2><strong>Why this matters</strong></h2>
<p>In a traditional approach, you'd need:</p>
<ul>
<li><p>Idempotency keys for every external call</p>
</li>
<li><p>A state machine tracking which steps completed</p>
</li>
<li><p>Compensating transactions for rollbacks</p>
</li>
<li><p>Retry logic with exponential backoff per step</p>
</li>
<li><p>A dead-letter queue for permanently failed orders</p>
</li>
</ul>
<p>With durable workflows, you get all of this for free. The engine handles checkpointing, retries, and resumption. You just define the steps and their dependencies.</p>
<h2><strong>The full code</strong></h2>
<pre><code class="language-typescript">import {
  task, flow, branch,
  runDurableWorkflow, resumeWorkflow, InMemoryBackend,
} from "sayiir";

let shippingDown = true;

// ── Tasks ──

const validateOrder = task("validate-order", (order) =&gt; {
  console.log(`  [validate-order] Order ${order.id} is valid`);
  return { ...order, validated: true };
});

const chargePayment = task("charge-payment", (order) =&gt; {
  console.log(`  [charge-payment] Charged $${order.amount} → pay_${order.id}`);
  return { paymentId: `pay_${order.id}`, amount: order.amount };
}, { timeout: "30s", retry: { maxAttempts: 3, initialDelay: "1s", backoffMultiplier: 2.0 } });

const reserveInventory = task("reserve-inventory", (order) =&gt; {
  console.log(`  [reserve-inventory] Reserved 1× ${order.item}`);
  return { item: order.item, reserved: true };
});

const arrangeShipping = task("arrange-shipping", ([payment, inventory]) =&gt; {
  if (shippingDown) throw new Error("Shipping API is down!");
  const trackingId = "TRACK-" + Math.random().toString(36).slice(2, 8).toUpperCase();
  console.log(`  [arrange-shipping] Shipping arranged → ${trackingId}`);
  return { paymentId: payment.paymentId, item: inventory.item, trackingId };
});

const sendConfirmation = task("send-confirmation", (result) =&gt; {
  console.log(`  [send-confirmation] Email sent — tracking ${result.trackingId}`);
  return `Order complete! \({result.item} ships via \){result.trackingId}`;
});

// ── Workflow ──

const workflow = flow("order-saga")
  .then(validateOrder)
  .fork([
    branch("payment", chargePayment),
    branch("inventory", reserveInventory),
  ])
  .join("merge", arrangeShipping)
  .then(sendConfirmation)
  .build();

// ── Run ──

const backend = new InMemoryBackend();
// For production: const backend = PostgresBackend.connect(process.env.DATABASE_URL);

const order = { id: "ORD-42", item: "Wireless Keyboard", amount: 79.99 };

// Run 1 — shipping is down
console.log("=== Run 1: Shipping API is down ===\n");
const run1 = runDurableWorkflow(workflow, `saga-${order.id}`, order, backend);
console.log(`\nStatus: ${run1.status}`);
if (run1.status === "failed") console.log(`Error: ${run1.error}`);

// "Fix" the shipping service
console.log("\n--- Shipping API recovered ---\n");
shippingDown = false;

// Run 2 — resume from last checkpoint
console.log("=== Run 2: Resume from checkpoint ===\n");
const run2 = resumeWorkflow(workflow, `saga-${order.id}`, backend);
console.log(`\nStatus: ${run2.status}`);
if (run2.status === "completed") console.log(`Result: ${run2.output}`);
</code></pre>
<h2><strong>Try it yourself</strong></h2>
<ul>
<li><p><strong>Playground</strong> — run the order saga in your browser: <a href="https://sayiir.dev/playground">sayiir.dev/playground</a></p>
</li>
<li><p><strong>Docs</strong> — <a href="http://sayiir.dev">sayiir.dev</a></p>
</li>
<li><p><strong>npm</strong> — <code>npm install sayiir</code></p>
</li>
<li><p><strong>PyPI</strong> — <code>pip install sayiir</code></p>
</li>
<li><p><a href="http://crates.io"><strong>crates.io</strong></a> — <a href="https://crates.io/crates/sayiir-runtime">https://crates.io/crates/sayiir-runtime</a></p>
</li>
</ul>
<hr />
<p><em>Sayiir is an open-source durable workflow engine with a Rust core and bindings for Node.js and Python. Star it on</em> <a href="https://github.com/sayiir-org/sayiir"><em>GitHub</em></a> <em>if you find it useful.</em></p>
]]></content:encoded></item></channel></rss>