Durable Functions Patterns

Earn 25 points (50 with Pro) in two steps

  1. ① Read through the lesson — each section gets a ✓ as you scroll through it.
  2. ② When every section has a ✓, tap Complete lesson.

0 of 11 read · keep scrolling

✦ See fewer ads and earn double points — 50 a lesson instead of 25 — with Pro

Lesson: Mastering Durable Functions Patterns

Introduction: Why Durable Functions Matter

In the world of cloud-native development, we frequently encounter workflows that span multiple steps, require state management, or demand long-running execution. Standard Azure Functions follow a stateless, event-driven model; they are designed to wake up, perform a quick task, and shut down. While this is efficient for simple microservices, it falls short when you need to coordinate complex business logic, such as processing an order that requires inventory checks, payment processing, and shipping notifications.

This is where Durable Functions come into play. Durable Functions are an extension of Azure Functions that allow you to write stateful workflows in a serverless environment. By using the Durable Task Framework, they manage state, checkpoints, and restarts automatically behind the scenes. This capability is essential because it allows developers to write complex asynchronous code that looks and behaves like synchronous code, removing the need for manual database state tracking or complex queue management. Understanding these patterns is not just about writing code; it is about building reliable, maintainable systems that can recover from failures without losing progress.


Not read yet

Understanding the Orchestration Model

At the heart of every Durable Function project is the Orchestrator Function. This function defines the workflow using code rather than JSON or XML configuration files. When an orchestrator runs, it is essentially a state machine that tracks the progress of the workflow. Because orchestrator functions can be restarted (replayed), they must be deterministic. This means that if you run the same code with the same inputs, you must always get the same outputs.

Callout: Determinism in Orchestrations The most critical rule in Durable Functions is that orchestrator code must be deterministic. Because the orchestrator re-executes its code every time a task completes or a message arrives, you cannot use non-deterministic operations like DateTime.Now, Guid.NewGuid(), or random number generation directly in your orchestrator code. If you need these values, you must obtain them from the context object provided by the Durable Framework, which ensures the value is consistent across replays.


Not read yet

Pattern 1: Function Chaining

Function chaining is the most fundamental pattern in the Durable Functions library. It involves executing a sequence of functions in a specific order, where the output of one function serves as the input for the next. In a standard Azure Functions environment, this would require a series of queues or storage triggers, which creates a "spaghetti" of infrastructure to manage.

Practical Example: The Order Processing Pipeline

Imagine an e-commerce application where an order must go through three distinct phases: validation, payment, and fulfillment. Using the chaining pattern, we can orchestrate these as a single, readable piece of code.

[FunctionName("OrderProcessingOrchestrator")]
public static async Task<string> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    // Step 1: Validate the order
    var order = context.GetInput<Order>();
    var validationResult = await context.CallActivityAsync<bool>("ValidateOrder", order);
    
    if (!validationResult) return "Order Invalid";

    // Step 2: Process Payment
    var paymentResult = await context.CallActivityAsync<string>("ProcessPayment", order);
    
    // Step 3: Ship the item
    var shippingResult = await context.CallActivityAsync<string>("ShipOrder", paymentResult);
    
    return shippingResult;
}

In this example, the orchestrator pauses at every await statement. When ValidateOrder finishes, the Durable Task Framework saves the state and wakes up the orchestrator to proceed to ProcessPayment. If the server crashes during this time, the workflow resumes exactly where it left off.


Not read yet

Pattern 2: Fan-Out/Fan-In

The Fan-Out/Fan-In pattern is used to execute multiple functions in parallel and then wait for all of them to finish before performing a final aggregation step. This is common in scenarios like batch processing, image resizing, or generating reports from multiple data sources.

Implementing Parallel Execution

Instead of running tasks sequentially, we start multiple tasks and store their handles in a list. We then use Task.WhenAll to wait for the entire collection to complete.

[FunctionName("BatchProcessor")]
public static async Task<long> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var workItems = await context.CallActivityAsync<List<string>>("GetWorkItems", null);
    var tasks = new List<Task<long>>();

    foreach (var item in workItems)
    {
        // Fan-out: Start tasks in parallel
        Task<long> task = context.CallActivityAsync<long>("ProcessItem", item);
        tasks.Add(task);
    }

    // Fan-in: Wait for all to finish
    long[] results = await Task.WhenAll(tasks);
    
    return results.Sum();
}

This pattern is highly efficient because Azure Functions will scale out the worker instances to handle the parallel tasks, significantly reducing the total execution time compared to a linear loop.


Not read yet

Pattern 3: Async HTTP APIs

One of the biggest challenges in distributed systems is handling long-running operations triggered by HTTP requests. If a client sends an HTTP POST request to a process that takes 10 minutes to complete, the connection will likely timeout. The Async HTTP API pattern solves this by providing the client with a status endpoint.

The Workflow Process

  1. The client sends a request to an HTTP trigger function.
  2. The trigger starts an orchestrator and returns a "202 Accepted" response.
  3. The response includes a Location header or a body field containing a status URL.
  4. The client polls the status URL until the orchestrator completes.

Note: The Durable Task Framework automatically generates these status endpoints for you. When you use the CreateCheckStatusResponse method in your HTTP starter, it creates a set of management URLs (status query, event raise, terminate) that you can hand off to your frontend client.


Not read yet

Pattern 4: Monitor Pattern

The Monitor pattern is used for scenarios where you need to poll an external service until a specific condition is met. Unlike a standard loop, the Monitor pattern is "durable," meaning it can survive host restarts and can wait for long periods (days or weeks) without incurring heavy costs.

Example: Waiting for an External System

Suppose you are waiting for a third-party API to report that a file upload is complete. You don't want to block a thread for hours. Instead, you create a monitor that sleeps for a set interval and checks the status periodically.

[FunctionName("MonitorJob")]
public static async Task RunMonitor(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var jobId = context.GetInput<string>();
    DateTime expiryTime = context.CurrentUtcDateTime.AddHours(24);

    while (context.CurrentUtcDateTime < expiryTime)
    {
        var status = await context.CallActivityAsync<string>("CheckExternalStatus", jobId);
        if (status == "Completed")
        {
            await context.CallActivityAsync("NotifySuccess", jobId);
            break;
        }

        // Wait for 30 minutes before checking again
        DateTime nextCheck = context.CurrentUtcDateTime.AddMinutes(30);
        await context.CreateTimer(nextCheck, CancellationToken.None);
    }
}

This approach is much more cost-effective than keeping a function running constantly, as the orchestrator is "unloaded" from memory during the CreateTimer wait period.


Not read yet

Pattern 5: Human Interaction

Many workflows require human intervention, such as approving an expense report or confirming a contract. This pattern involves pausing the orchestrator and waiting for an external event—like a click in an email or a manual entry in a portal—to resume the workflow.

Handling External Events

You use context.WaitForExternalEvent to pause the orchestrator. The orchestrator will remain in a "Running" state but will not consume CPU resources while waiting for the event.

[FunctionName("ApprovalWorkflow")]
public static async Task RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    await context.CallActivityAsync("SendApprovalEmail", null);

    // Wait for the 'ApprovalEvent'
    using (var timeoutCts = new CancellationTokenSource())
    {
        DateTime timeout = context.CurrentUtcDateTime.AddDays(3);
        Task timeoutTask = context.CreateTimer(timeout, timeoutCts.Token);
        Task<bool> approvalTask = context.WaitForExternalEvent<bool>("ApprovalEvent");

        Task winner = await Task.WhenAny(approvalTask, timeoutTask);
        if (winner == approvalTask)
        {
            timeoutCts.Cancel();
            await context.CallActivityAsync("ProcessApproval", approvalTask.Result);
        }
        else
        {
            await context.CallActivityAsync("HandleTimeout", null);
        }
    }
}

This pattern provides a robust way to handle "long-lived" processes that might take days to resolve, ensuring that the state is perfectly preserved throughout the wait.


Not read yet

Best Practices for Durable Functions

Writing Durable Functions requires a shift in mindset. Because the orchestrator code runs multiple times, you must adhere to strict guidelines to ensure the system remains reliable and performant.

1. Keep Orchestrators Lightweight

Orchestrators should only contain logic for coordinating tasks. They should not perform data processing, database queries, or complex calculations. These should be offloaded to Activity Functions. If you put heavy logic in the orchestrator, it will be executed repeatedly during every replay, which increases latency and cost.

2. Avoid Non-Deterministic Logic

As mentioned earlier, never use DateTime.Now or Guid.NewGuid() in an orchestrator. Always use the IDurableOrchestrationContext methods. If you need a random value, generate it in an Activity Function and pass it back to the orchestrator.

3. Use Idempotent Activity Functions

Activity functions may be retried if a failure occurs. Therefore, each activity should be idempotent—meaning that running it multiple times with the same input should produce the same result and not cause side effects. For example, a "ChargeCreditCard" function should check if the charge already occurred before attempting to charge the card again.

4. Manage Versioning

When you update your orchestrator code, existing instances that are currently in a "Running" or "Sleeping" state will continue to run using the old code. If you introduce breaking changes, you must version your orchestrator functions (e.g., OrderProcess_V2) to ensure that existing workflows complete successfully.

5. Monitor Execution Costs

Durable Functions incur costs based on storage transactions and execution time. While they are generally cheaper than keeping a VM running, excessive replay cycles or long-running timers can add up. Ensure you use appropriate polling intervals in the Monitor pattern to avoid unnecessary costs.


Not read yet

Common Pitfalls and How to Avoid Them

Even experienced developers can run into issues with Durable Functions. Here are the most frequent mistakes:

  • Blocking Calls: Never use Thread.Sleep or synchronous I/O operations in an orchestrator. These block the underlying thread and prevent the framework from properly managing the lifecycle of the function. Always use await with asynchronous methods.
  • Infinite Loops without Yielding: An orchestrator loop that doesn't include an await on an activity, timer, or event will cause the function to run forever, eventually crashing the host. Ensure every loop iteration has a point where the orchestrator yields control back to the framework.
  • Ignoring Exception Handling: Because orchestrations are distributed, exceptions can happen at any step. Use try-catch blocks around your CallActivityAsync calls to handle errors gracefully, perhaps by implementing custom retry policies.
  • Large Payloads: Avoid passing massive objects as inputs or outputs to activity functions. The data is serialized into Azure Storage. If you exceed the limits, your orchestration will fail. Instead, pass a reference or an ID and have the activity function fetch the necessary data from a database or blob storage.

Not read yet

Quick Reference: Pattern Comparison

Pattern Best Use Case Key Mechanism
Chaining Sequential steps await calls in sequence
Fan-Out/In Batch processing Task.WhenAll
Async HTTP Long-running APIs CreateCheckStatusResponse
Monitor Periodic polling CreateTimer + loop
Human Interaction Approvals/Manual tasks WaitForExternalEvent

Step-by-Step: Setting Up Your First Durable Function

To start working with these patterns in your own environment, follow these steps:

  1. Install the Extension: Ensure your Azure Functions project has the Microsoft.Azure.WebJobs.Extensions.DurableTask NuGet package installed.
  2. Define the Starter: Create an HTTP-triggered function that uses the IDurableClient binding to start a new orchestration instance.
  3. Create the Orchestrator: Add a function with the [OrchestrationTrigger] attribute. Define your workflow logic here.
  4. Create Activity Functions: Add functions with the [ActivityTrigger] attribute to perform the actual work (database writes, API calls, etc.).
  5. Test Locally: Use the Azure Functions Core Tools to run the project. Open the localhost URL provided, and the framework will automatically generate the status endpoints for your orchestration instances.

Tip: When testing locally, use the Azure Storage Emulator (or Azurite) to manage the state tables and queues that Durable Functions require. This allows you to inspect the orchestration state directly in your storage explorer.


Not read yet

Summary and Key Takeaways

Durable Functions provide a powerful way to handle complex workflows without the infrastructure overhead of traditional workflow engines. By mastering these patterns, you can build systems that are resilient, scalable, and easy to reason about.

Key Takeaways:

  1. Orchestrator Determinism: Always treat orchestrator functions as pure state machines. Avoid non-deterministic values like current time or random numbers; use the context provided by the framework instead.
  2. Asynchronous by Design: Embrace the await keyword. Orchestrators are designed to pause and resume, allowing you to handle processes that span minutes, hours, or even days.
  3. Pattern Selection: Match your business problem to the right pattern. Use Chaining for sequence, Fan-Out/In for parallelism, and the Monitor or Human Interaction patterns for long-running processes.
  4. Idempotency is Essential: Since activities can be retried, ensure your functions are idempotent. This prevents duplicate charges, double-processed database entries, or other unintended side effects.
  5. Infrastructure Efficiency: Use the Monitor pattern instead of persistent loops to save on compute costs. The framework effectively "dehydrates" your function when it is waiting, ensuring you only pay for the time the code is actively executing.
  6. Version Management: Plan for code updates by versioning your orchestrators. This prevents breaking changes from affecting workflows that are currently in progress.
  7. Data Strategy: Keep inputs and outputs small. Pass identifiers rather than large objects to keep your storage transactions low and performance high.

By applying these patterns and best practices, you can shift from writing fragile, manual state-management code to building robust, automated workflows that leverage the full power of the Azure serverless ecosystem. Focus on keeping your orchestrators clean, your activities idempotent, and your logic deterministic, and you will find that even the most complex business processes become manageable and reliable.

Not read yet

Each section gets a ✓ as you scroll through it. Tap the button to jump to the next one.