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 12 read · keep scrolling

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

Mastering Durable Functions Patterns in Azure

Introduction: Why Durable Functions Matter

When building cloud-native applications, we often encounter workflows that are long-running, stateful, and require complex coordination between multiple services. Traditional Azure Functions are stateless by design; they are built to be triggered by an event, execute a quick piece of logic, and then terminate. While this "serverless" model is excellent for high-throughput, short-lived tasks, it falls short when you need to track the state of a multi-step business process that might take minutes, hours, or even days to complete.

This is where Durable Functions come into play. Durable Functions is an extension of Azure Functions that allows you to write stateful workflows in code. By using the Durable Task Framework, these functions manage state, checkpoints, and restarts behind the scenes, allowing you to write complex, asynchronous code that looks and behaves like synchronous, sequential code. This paradigm shift—moving from managing distributed state manually in databases to defining it through code logic—is a game-changer for developers building microservices, integration pipelines, and complex background processing systems.

In this lesson, we will explore the core design patterns of Durable Functions. Understanding these patterns is not just about learning syntax; it is about learning how to architect resilient, fault-tolerant systems in a serverless environment. Whether you are orchestrating a payment process, scraping data from multiple sources, or managing a human-in-the-loop approval workflow, these patterns provide the blueprint for success.


Not read yet

The Four Core Roles in Durable Functions

Before diving into the patterns, it is vital to understand the four types of functions involved in a Durable Functions project. Each has a specific responsibility:

  1. Client Functions: These are the entry points. They are triggered by HTTP requests, queues, or other events and are responsible for starting or managing the orchestrator.
  2. Orchestrator Functions: This is the heart of the system. They define the workflow logic using code. They are "replayable," meaning they can be paused and resumed, and their state is automatically persisted.
  3. Activity Functions: These are the "worker" functions. They perform the actual work (e.g., calling an API, querying a database, or performing a calculation). They are stateless and can be scaled independently.
  4. Entity Functions: These represent small, stateful objects (entities). They are useful for scenarios where you need to track the state of a specific object, like a shopping cart or a game session, and update it frequently.

Callout: Orchestrators vs. Activity Functions The most important rule to remember is that orchestrators must be deterministic. Because the orchestrator re-runs its code every time it resumes from an await statement, it must always produce the same result for the same input. You should never perform I/O, generate random numbers, or get the current time inside an orchestrator. Always offload these non-deterministic tasks to Activity Functions.


Not read yet

Pattern 1: Function Chaining

Function chaining is the most straightforward pattern. It involves executing a sequence of functions in a specific order. The output of one function becomes the input for the next. In a standard stateless environment, you would have to manage this by passing messages through queues, which creates "spaghetti" logic where it is hard to track where the process is at any given time.

Practical Example: Order Processing

Imagine an e-commerce order process. You need to validate the order, charge the credit card, and update the inventory.

[FunctionName("OrderOrchestrator")]
public static async Task<string> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var order = context.GetInput<Order>();

    // Chain the activities
    var isValid = await context.CallActivityAsync<bool>("ValidateOrder", order);
    if (!isValid) return "Invalid Order";

    await context.CallActivityAsync("ChargeCreditCard", order);
    await context.CallActivityAsync("UpdateInventory", order);

    return "Order Processed Successfully";
}

Why this works

In this code, the await keyword pauses the orchestrator function. While the orchestrator is paused, the underlying compute resources are released. Once the activity function completes, the orchestrator wakes up, restores its state, and moves to the next line. This allows you to write complex business logic as if it were a simple try-catch block, even though the process might take hours to complete.


Not read yet

Pattern 2: Fan-Out/Fan-In

The Fan-Out/Fan-In pattern is used when you need to execute multiple functions in parallel and then wait for all of them to finish before proceeding. This is highly efficient for batch processing or parallel data retrieval.

Practical Example: Parallel Image Processing

Suppose you have a folder of images that need to be resized. Instead of processing them one by one, you can fan out the resizing task to multiple instances and then aggregate the results.

[FunctionName("ProcessImagesOrchestrator")]
public static async Task<List<string>> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var imageUrls = context.GetInput<List<string>>();
    var tasks = new List<Task<string>>();

    foreach (var url in imageUrls)
    {
        // Fan-out: Start all tasks in parallel
        Task<string> task = context.CallActivityAsync<string>("ResizeImage", url);
        tasks.Add(task);
    }

    // Fan-in: Wait for all tasks to complete
    string[] results = await Task.WhenAll(tasks);
    return results.ToList();
}

Note: When using Task.WhenAll in an orchestrator, you are effectively telling the Durable Task Framework to track multiple concurrent operations. The framework ensures that the orchestrator only resumes once every single task in the list has signaled completion.


Not read yet

Pattern 3: Async HTTP APIs (Polling Pattern)

One of the most common challenges in cloud development is handling long-running HTTP requests. If you have an API endpoint that triggers a 10-minute task, the client will likely timeout before receiving a response. The Async HTTP API pattern solves this by returning a "202 Accepted" response with a status URL.

The Workflow

  1. Client sends a request to the API.
  2. API starts the orchestrator and returns a status URL.
  3. Client polls the status URL periodically.
  4. Orchestrator updates the status (Running, Completed, Failed).
  5. Client receives the final result once the task is finished.

Durable Functions handles this automatically. When you trigger an orchestrator via an HTTP-started function, the framework provides built-in status endpoints that you can return to the client.


Pattern 4: Monitor Pattern

The Monitor pattern is used for scenarios where you need to repeatedly poll an external resource until a specific condition is met. Think of checking a weather API for a specific condition or waiting for a long-running external job to signal completion.

Practical Example: Waiting for a Job Status

Unlike the Fan-Out/Fan-In pattern, which happens immediately, the Monitor pattern often spans a long duration with significant delays between checks.

[FunctionName("MonitorJobOrchestrator")]
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>("CheckJobStatus", jobId);
        if (status == "Completed") break;

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

Warning: Avoid using Thread.Sleep or Task.Delay in your orchestrator. These methods are not durable and will cause your orchestrator to fail because they do not persist the state of the timer. Always use context.CreateTimer to ensure the orchestrator can "sleep" and resume correctly.


Not read yet

Pattern 5: Human Interaction (Approval Workflows)

Many enterprise processes require human intervention. For example, an expense report might need a manager's approval. The Human Interaction pattern pauses the orchestrator and waits for an external event, such as an email approval or a webhook call, to arrive.

Implementation Strategy

  1. The orchestrator sends an email with a link containing a unique event ID.
  2. The orchestrator calls context.WaitForExternalEvent("ApprovalEvent").
  3. The orchestrator pauses indefinitely until the event is raised.
  4. The manager clicks the link, which hits an API that calls client.RaiseEventAsync.
  5. The orchestrator wakes up, receives the approval, and continues.

Comparison Table: Durable Functions Patterns

Pattern Primary Use Case Key Mechanism
Chaining Sequential processing await calls
Fan-Out/In Batch processing Task.WhenAll
Async HTTP Long-running APIs Status polling URLs
Monitor Periodic checking context.CreateTimer
Human Interaction Approvals/Events WaitForExternalEvent

Not read yet

Best Practices for Durable Functions

1. Maintain Determinism

As mentioned earlier, the orchestrator function code is re-run multiple times to restore state. If you put DateTime.Now inside your orchestrator, every time it replays, the time will be different, leading to corrupted state.

  • Best Practice: Always use context.CurrentUtcDateTime in orchestrators.
  • Best Practice: Use activity functions for all data retrieval, random number generation, or API calls.

2. Keep Orchestrators Lightweight

Orchestrators should contain only business logic and orchestration code. They should not perform heavy data processing or blocking I/O operations.

  • Tip: If you find yourself doing complex JSON parsing or data transformation inside an orchestrator, move that logic into an Activity Function. This keeps your orchestrator clean and prevents "replay" overhead.

3. Handle Versioning

When you update your orchestrator code, existing instances that are already running will continue to use the old logic. If you make a breaking change (like changing the order of await calls), those instances will fail during replay.

  • Strategy: Use side-by-side versioning by changing the function name (e.g., Orchestrator_V2) or by using the [Version] attribute if your framework supports it. Always test your code for replay compatibility.

4. Optimize Activity Functions

Activity functions are the "workers" that consume compute resources. Since they are triggered by messages in an internal storage queue, they are naturally throttled by the number of instances you have running.

  • Best Practice: Ensure your activity functions are idempotent. Because of the nature of distributed systems, there is a small chance an activity function might run more than once. Design your functions so that running them twice with the same input does not cause negative side effects (e.g., duplicate database entries).

5. Managing Large Payloads

Durable Functions use Azure Storage to persist state. If you pass massive objects as inputs or outputs, you will hit the limits of your storage provider and increase latency.

  • Best Practice: Store large data in Blob Storage and pass only the URL or the record ID to the orchestrator. Keep the state payloads as small as possible.

Not read yet

Common Pitfalls and How to Avoid Them

Pitfall 1: The "Infinite Loop"

It is easy to accidentally create an orchestrator that triggers itself or creates a loop that never terminates. Because Durable Functions can persist state for a long time, an infinite loop can consume significant storage and compute costs.

  • Solution: Always implement a termination condition or a maximum retry count. Use the DurableOrchestrationContext.CurrentUtcDateTime to set hard timeouts for long-running processes.

Pitfall 2: Ignoring Exception Handling

When an activity function fails, it throws an exception that propagates to the orchestrator. If you don't catch it, the entire orchestrator instance will fail.

  • Solution: Use try-catch blocks inside your orchestrator. You can implement custom retry policies using RetryOptions when calling activity functions to handle transient network errors gracefully.
var retryOptions = new RetryOptions(
    firstRetryInterval: TimeSpan.FromSeconds(5),
    maxNumberOfAttempts: 3);

await context.CallActivityWithRetryAsync("ProcessData", retryOptions, data);

Pitfall 3: Overusing Durable Entities

Durable Entities are powerful for state management, but they are not a replacement for a database. Using entities to store thousands of records will lead to performance issues.

  • Solution: Use Durable Entities for "hot" state (e.g., a counter, a user's current session score, or a toggle) and use a database like Cosmos DB for persistent, long-term storage.

Not read yet

Step-by-Step: Implementing a Simple Chaining Pattern

To reinforce what we have learned, let's walk through the creation of a basic chaining workflow.

Step 1: Create the Activity Function

This function will perform the unit of work.

[FunctionName("SayHello")]
public static string SayHello([ActivityTrigger] string name)
{
    return $"Hello {name}!";
}

Step 2: Create the Orchestrator

This function coordinates the activities.

[FunctionName("HelloOrchestrator")]
public static async Task<List<string>> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var outputs = new List<string>();
    outputs.Add(await context.CallActivityAsync<string>("SayHello", "Tokyo"));
    outputs.Add(await context.CallActivityAsync<string>("SayHello", "Seattle"));
    outputs.Add(await context.CallActivityAsync<string>("SayHello", "London"));
    return outputs;
}

Step 3: Create the Client

This function triggers the orchestrator.

[FunctionName("HttpStart")]
public static async Task<HttpResponseMessage> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestMessage req,
    [DurableClient] IDurableOrchestrationClient starter)
{
    string instanceId = await starter.StartNewAsync("HelloOrchestrator", null);
    return starter.CreateCheckStatusResponse(req, instanceId);
}

Step 4: Testing

  1. Deploy the code to your Azure Function App.
  2. Trigger the HttpStart function.
  3. The response will contain a statusQueryGetUri.
  4. Copy that URL into your browser to see the current status and, eventually, the final output of the orchestrator.

Not read yet

Advanced Topic: Durable Entities

While we have focused on orchestrators, Durable Entities are a significant part of the Durable Functions landscape. They are effectively "stateful actors." Each entity has a unique ID and a state that is persisted.

Why use Entities?

Entities are useful when you need to perform "atomic" updates to a piece of state. For example, if you have a system where multiple users are updating a shared counter, a regular database approach might involve a race condition. With an Entity, all operations are serialized, meaning only one operation happens at a time for that specific entity, ensuring consistency.

Example: A Simple Counter

[FunctionName("Counter")]
public static Task Counter([EntityTrigger] IDurableEntityContext context)
{
    int currentValue = context.GetState<int>();
    switch (context.OperationName)
    {
        case "add":
            currentValue += context.GetInput<int>();
            break;
        case "reset":
            currentValue = 0;
            break;
    }
    context.SetState(currentValue);
    return Task.CompletedTask;
}

This pattern allows you to maintain stateful objects in a serverless way, without needing to manage a dedicated cache or database for every small bit of transient data.


Not read yet

Frequently Asked Questions (FAQ)

Q: Are Durable Functions expensive? A: Durable Functions use Azure Storage to checkpoint state. You will be billed for the storage consumed and the execution time of your functions. Generally, they are very cost-effective, but if you have thousands of orchestrators running simultaneously, keep an eye on your storage costs.

Q: Can I call an external API from an orchestrator? A: Technically, yes, but you should not. If the API call is non-deterministic (e.g., returns different data each time), it will break your orchestrator when it replays. Always wrap API calls in an Activity Function.

Q: How do I debug Durable Functions? A: Use the Azure Functions Core Tools locally. You can see the orchestration history in the console. For production, use Application Insights to track the dependencies and execution flow of your orchestrations.

Q: What happens if an orchestrator stays alive for a month? A: Durable Functions are designed for this. As long as your code is deterministic and handles exceptions, the framework will manage the state effectively. However, ensure your code is updated to handle potential platform changes or API deprecations over that duration.


Not read yet

Key Takeaways

  1. State Management: Durable Functions move the burden of state management from your application logic to the framework, allowing you to focus on business processes rather than infrastructure.
  2. Determinism is King: An orchestrator is replayed multiple times. Never use non-deterministic code (random numbers, time, I/O) directly in an orchestrator; always delegate these to Activity Functions.
  3. Use the Right Pattern: Match your business requirements to the correct pattern. Chaining for sequences, Fan-Out/In for parallel tasks, and Monitor for polling are the most common tools in your kit.
  4. Async/Await is Your Friend: The power of Durable Functions lies in the ability to use standard C# asynchronous patterns to represent long-running workflows that span days or weeks.
  5. Always Think About Replays: When writing orchestrators, ask yourself: "If this code ran again from the beginning, would it produce the exact same result?" If the answer is no, you have a bug.
  6. Idempotency Matters: Because of the distributed nature of cloud functions, always design your activity functions so that they can be safely executed multiple times without negative side effects.
  7. Monitor and Manage: Use built-in status endpoints to provide feedback to your users and utilize Application Insights to monitor the health and performance of your orchestrations in production.

By mastering these patterns, you are not just writing functions; you are building robust, resilient systems capable of handling the most complex business processes. Start by implementing a simple chaining pattern, then experiment with Fan-Out/In, and you will quickly see the power and flexibility that Durable Functions brings to your Azure compute solutions.

Not read yet

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