AWS Lambda Durable Functions vs Step Functions

AWS Lambda Durable Functions allow developers to write stateful, multi-step workflows entirely within code, removing the need for external orchestrators for many use cases. In this tutorial, we will build a classic e-commerce pattern—Order Processing—handling validation, payment, and inventory checks with built-in retries and checkpoints.

Key Takeaways

In this tutorial, we will cover the following concepts:

  • The DurableContext: How to inject the coordination context into your handler (Node.js and Python).
  • Steps and Checkpoints: Using context.step to persist results and avoid re-executing logic.
  • Waits: Suspending execution without billing using context.wait.
  • Code Structure: Comparing the syntax between TypeScript/Node.js and Python.

The Order Processing Workflow

We are going to orchestrate a workflow that validates an order, processes a payment, waits for a brief confirmation period, and then confirms the order. If the function crashes or pauses, it resumes exactly where it left off.

Node.js / TypeScript Implementation

For Node.js (specifically runtimes like Node.js 24), we use the withDurableExecution wrapper. This injects the context object we need to manage state.

import { withDurableExecution } from "@aws/durable-execution-sdk-js"; export const handler = withDurableExecution(async (event, context) => { const orderId = event.orderId; // Step 1: Validate the order. The result is checkpointed. const validation = await context.step("validate-order", async () => { // Assume customerService is an imported module return await customerService.validate(event.customerId); }); if (!validation.isValid) { return { status: "rejected", reason: "Invalid Customer" }; } // Step 2: Process Payment. // If the function fails after this, payment is not charged twice. const payment = await context.step("process-payment", async () => { return await paymentService.charge(orderId, event.amount); }); // Step 3: Wait. // Execution suspends here. You are NOT billed for these 10 seconds. await context.wait({ seconds: 10 }); // Step 4: Confirm Order await context.step("confirm-order", async () => { return await inventoryService.confirm(orderId); }); return { status: "completed", orderId }; });

Python Implementation

If you prefer Python (specifically Python 3.14), the pattern uses decorators. The @durable_execution decorator handles the context injection, and @durable_step can be used on helper functions.

from aws_durable_execution_sdk_python import DurableContext, durable_execution, durable_step from aws_durable_execution_sdk_python.config import Duration @durable_step def validate_order(order_id): # Logic to validate order return {"status": "valid"} @durable_execution def lambda_handler(event, context: DurableContext): order_id = event['orderId'] # Step 1: Call the decorated step function # Note: context.step is used to wrap inline logic or calls validation = context.step( lambda: validate_order(order_id), name="validate-order" ) # Step 2: Wait using the Duration config context.wait(Duration.from_seconds(10)) # Step 3: Finalize result = context.step( lambda: {"status": "confirmed", "id": order_id}, name="finalize" ) return result

How Replay Works

When the context.wait finishes (after 10 seconds), Lambda re-invokes your handler. It runs the code from the top. However, when it hits “validate-order” and “process-payment,” it sees those checkpoints exist. It skips the actual execution of those functions and immediately returns the stored result. The code only “runs” for the first time at lines following the wait.

Conclusion

We demonstrated how to build a multi-step workflow using simple code primitives. By wrapping side effects in steps, we ensure they are performed exactly once, and by using wait, we can pause execution for up to one year without paying for idle compute.