Traffic and messaging
Durable workflows
How to run a multi-step process (charge, reserve, ship, notify) that must survive crashes, retries and undo steps, using a workflow engine instead of chained queues.
A single job is easy: send one email, resize one image. A process with several steps is harder. Place an order means charge the card, reserve stock, create a shipment and send an email. If step 3 fails, step 1 must be undone. If the server crashes in the middle, the process must resume, not restart from scratch.
The big idea
You can chain queue jobs. Step 1 finishes and publishes a message for step 2, and so on. This works, but the state of the process ends up spread across queues, database flags and logs. Nobody can answer "where is order 881?" without searching all three.
A durable workflow engine solves this. You write the process as ordinary code. The engine records the result of each step. If the process crashes, the engine replays the code and skips the steps that already finished.
Concept
A step (or activity) is one action with a side effect, such as charging a card. A workflow is the code that decides which steps to run, in what order, and what to do if one fails.
Walking through an order
Here is a sketch of a Temporal workflow in TypeScript. Each activity is retried by the engine, and the result is saved.
import { proxyActivities } from "@temporalio/workflow"
import type * as acts from "./activities"
const { chargeCard, reserveStock, refundCard, createShipment } = proxyActivities<typeof acts>({
startToCloseTimeout: "30 seconds",
retry: { maximumAttempts: 5, backoffCoefficient: 2 },
})
export async function placeOrder(order: Order): Promise<void> {
const charge = await chargeCard(order.id, order.total)
try {
await reserveStock(order.id, order.items)
} catch (err) {
await refundCard(charge.id) // compensation: undo the earlier step
throw err
}
await createShipment(order.id)
}Read it top to bottom:
- The card is charged. The engine saves the charge result.
- Stock reservation fails. The workflow runs the compensation, a refund, which undoes step 1.
- If the server crashes after step 1, the engine resumes here and does not charge again.
This pattern is a saga: a sequence of steps, each with an undo.
Tip
Write each activity so it can be retried safely. The engine will retry it, so the activity must be idempotent.
When to use which
| Situation | Use |
|---|---|
| Send one email or resize one image | A plain job queue (Sidekiq, BullMQ, Celery, or a cloud queue) |
| Several steps, each may fail, some need undo | A workflow engine (Temporal, AWS Step Functions) |
| Scheduled and delayed single jobs | A job queue with scheduling |
Job queues add retries, delays, priorities and a dashboard. They are the right tool for single jobs, and most application teams use them before they need a workflow engine.
Common mistakes
- Using a queue for multi-step state. Chained jobs make failures hard to trace. Use a workflow when order matters across steps.
- Forgetting compensation. If you can't undo a step, say so in the design, and put the risky step last.
- Non-idempotent activities. The engine will retry them. A charge that isn't idempotent can charge twice.
Key takeaways
- A workflow engine records each step's result, so a crash resumes the process instead of restarting it.
- Use an undo step (compensation) when a later step fails.
- Single jobs belong in a job queue. Multi-step processes with undo belong in a workflow engine.
Further learning
The Temporal documentation has tutorials for workflows, activities and retries.
Related
Retries and dead-letter queues
How to retry a failing message with growing delays, when to stop, and where to put messages that will never succeed so the rest of the queue keeps moving.
Distributed transactions
Keeping data consistent when one business operation spans several databases or services, using two-phase commit, sagas, the transactional outbox and reservations, and redrawing boundaries first.
Message queues
A durable buffer between the code that creates work and the code that does it. Start here for the map of queue concepts.