Durable Functions Patterns
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks and see fewer ads — read each lesson on a single page 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.
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:
- 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.
- 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.
- 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.
- 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.
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.
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.WhenAllin 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.
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
- Client sends a request to the API.
- API starts the orchestrator and returns a status URL.
- Client polls the status URL periodically.
- Orchestrator updates the status (Running, Completed, Failed).
- 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.SleeporTask.Delayin 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 usecontext.CreateTimerto ensure the orchestrator can "sleep" and resume correctly.
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
- The orchestrator sends an email with a link containing a unique event ID.
- The orchestrator calls
context.WaitForExternalEvent("ApprovalEvent"). - The orchestrator pauses indefinitely until the event is raised.
- The manager clicks the link, which hits an API that calls
client.RaiseEventAsync. - 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 |
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.CurrentUtcDateTimein 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.
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.CurrentUtcDateTimeto 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-catchblocks inside your orchestrator. You can implement custom retry policies usingRetryOptionswhen 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.
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
- Deploy the code to your Azure Function App.
- Trigger the
HttpStartfunction. - The response will contain a
statusQueryGetUri. - Copy that URL into your browser to see the current status and, eventually, the final output of the orchestrator.
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.
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.
Key Takeaways
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles your daily points limit so you progress twice as fast, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× daily points limit
- ✓ Distraction-free lessons