Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 18

Durable Functions Architecture and Components

Recommended resources

Hello! Welcome back to our course on becoming an Azure Developer.

In our last lesson, we built a timer-triggered function to handle scheduled tasks. We saw how effective they are for stateless, recurring jobs like generating a report or cleaning up old data. However, those functions are designed to be short-lived and self-contained. They don't remember anything from one execution to the next.

This brings us to a crucial question: how do you manage complex processes that involve multiple steps, require state to be maintained over a long period, and need to coordinate various tasks?

This lesson will answer that by focusing on a powerful extension of Azure Functions. Today, we'll tackle the learning outcome: Describe the architecture and components of Durable Functions for stateful workflows.

We'll explore:

  • The challenges of building stateful applications with traditional, stateless functions.
  • The core architectural components of a Durable Function application.
  • The underlying mechanism that allows Durable Functions to maintain state in a serverless environment.
  • The critical coding constraints you must follow when writing orchestration logic.

Let's begin by understanding the problem that Durable Functions were designed to solve.

1. The Challenge of Stateful Orchestration

Standard Azure Functions are fundamentally stateless. This simplicity is a strength for many scenarios, but it becomes a significant challenge when you need to implement a workflow. Imagine trying to build an e-commerce order processing system with simple functions. You might need to:

  1. Check inventory.
  2. Process payment.
  3. Update the customer's order history.
  4. Create a shipping label.
  5. Send a confirmation email.

If any step fails, how do you retry it? If the payment step takes 10 minutes, how do you keep the process alive? How do you pass the state (like the order ID) reliably from one function to the next?

The following video segment by Jeff Hollan from the Microsoft Visual Studio team brilliantly outlines these common but difficult patterns in a serverless world.

S204 - Azure Durable Functions for serverless .NET orchestration - Jeff Hollan

This video explains the common stateful workflow problems that are difficult to solve with standard, stateless serverless functions, setting the stage for why Durable Functions are necessary.

Watch the section from 09:27 to 14:11. As you watch, focus on understanding these key patterns: Function Chaining / Sequencing: Executing a series of functions in a specific order. Fan-out/Fan-in: Running multiple functions in parallel and then aggregating their results. Async HTTP APIs: Handling long-running operations triggered by an HTTP request. Human Interaction: Pausing a workflow to wait for an external event, like an approval.

As you can see, trying to build these patterns manually would involve complex, custom code using queues and database tables to manage state, track progress, and handle errors. It's possible, but it's brittle and detracts from focusing on the business logic.


2. Introducing Durable Functions

Durable Functions is an extension for Azure Functions that solves these problems by providing a framework for writing stateful, long-running orchestrations directly in code. Instead of you managing the state with external services, the Durable Functions framework does it for you behind the scenes.

This is a powerful concept because it allows you to write what looks like normal, sequential code, but the framework translates it into a reliable, distributed, and serverless workflow.

Durable Functions Architecture Overview
This diagram illustrates the high-level architecture of a Durable Function application, showing how a Starter Function initiates the Orchestrator, which in turn coordinates the work of one or more Activity Functions.

Let's break down the components shown in this diagram.


3. The Core Components of a Durable Function App

A Durable Function application is composed of several function types, each with a specific role. Understanding these roles is key to understanding the architecture.

To get an official overview, please start by reading this short document from Microsoft Learn.

Durable Functions types and features - Azure - Microsoft Learn

This official documentation page, 'Durable Functions types and features', introduces the different function types that make up a durable orchestration.

Read the sections for 'Orchestrator functions', 'Activity functions', and 'Client functions'. We will discuss Entity functions in a later, more advanced lesson. Focus on the primary role of each of these three types.

Now, let's solidify those concepts. A typical durable orchestration primarily involves three types of functions:

a) Client Function (The Starter)

A Client Function is the entry point for an orchestration. It's a regular Azure Function (e.g., HTTP-triggered, Queue-triggered) that uses a special DurableClient binding to start, manage, and query orchestration instances.

  • Role: To kick off a new workflow instance.
  • Trigger: Any standard trigger (HTTP, Queue, Timer, etc.).
  • Key Action: It calls StartNewAsync on the durable client, which creates a new instance of an orchestrator and returns a unique instanceId.

Your experience with C# and REST APIs is directly applicable here. Think of an HTTP-triggered client function as a standard API endpoint that starts a background job.

// This is a Client Function
[FunctionName("StartOrderProcessing")]
public static async Task<IActionResult> HttpStart(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
    [DurableClient] IDurableOrchestrationClient starter)
{
    // 1. Get input from the HTTP request
    var orderData = await req.ReadFromJsonAsync<Order>();
    
    // 2. Start a new orchestration instance
    string instanceId = await starter.StartNewAsync("ProcessOrderOrchestrator", orderData);

    // 3. Return a response with management URLs
    return starter.CreateCheckStatusResponse(req, instanceId);
}

b) Orchestrator Function (The Conductor)

The Orchestrator Function is the heart and soul of the workflow. It defines the business logic and the sequence of operations. It is written in code but does not perform the actual work. Instead, it "conducts" the orchestra by calling Activity Functions.

  • Role: To define the workflow, manage state, and call activity functions.
  • Trigger: A special OrchestrationTrigger. It can only be triggered by a client function or another orchestrator.
  • Key Action: Uses the IDurableOrchestrationContext to call activities, wait for events, and manage timers.
// This is an Orchestrator Function
[FunctionName("ProcessOrderOrchestrator")]
public static async Task<List<string>> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var order = context.GetInput<Order>();
    var outputs = new List<string>();

    // 1. Call the first activity function
    outputs.Add(await context.CallActivityAsync<string>("CheckInventory", order.ProductId));
    
    // 2. Call the second activity function
    bool paymentSuccess = await context.CallActivityAsync<bool>("ProcessPayment", order);

    if (paymentSuccess)
    {
        // 3. Call a third activity if payment was successful
        outputs.Add(await context.CallActivityAsync<string>("SendConfirmationEmail", order.CustomerEmail));
    }
    else
    {
        outputs.Add("Payment failed. Halting orchestration.");
    }

    return outputs;
}

c) Activity Function (The Worker)

An Activity Function is where the actual work gets done. These are the individual, discrete tasks that the orchestrator calls.

  • Role: To perform a single, specific task (e.g., call a database, process a file, invoke an external API).
  • Trigger: A special ActivityTrigger. It can only be called by an orchestrator function.
  • Key Action: Performs potentially long-running or I/O-intensive operations and can return a result back to the orchestrator.

Unlike orchestrators, activity functions have no restrictions on the type of code they can run. This is where you put all your non-deterministic logic.

// This is an Activity Function
[FunctionName("ProcessPayment")]
public static bool ProcessPayment([ActivityTrigger] Order order, ILogger log)
{
    log.LogInformation($"Processing payment for order {order.OrderId}.");
    
    // Connect to a payment gateway API...
    // This is where real work, like network calls, happens.
    bool success = PaymentGateway.Charge(order.Amount, order.CreditCard);

    return success;
}

4. How It Works: The Magic of Event Sourcing and Replay

You might be wondering: if the orchestrator is just a serverless function, how does it maintain state when it's not running? The answer lies in a pattern called Event Sourcing.

The Durable Functions framework uses an Azure Storage account (known as the Task Hub) to automatically save the entire history of an orchestration.

  • When an orchestrator calls CallActivityAsync, it doesn't actually execute the activity. Instead, it sends a message to a queue and then effectively "goes to sleep" (deallocates), saving you money.
  • When the activity function finishes, it writes the result back to the storage account and sends a message to wake the orchestrator up.
  • When the orchestrator wakes up, it replays its code from the beginning. However, this time, when it reaches the CallActivityAsync it already executed, it doesn't call the activity again. Instead, it finds the result in its persisted history and immediately returns it. The code then continues from where it left off.

This replay mechanism is the core concept that enables durable, long-running, and stateful workflows in a stateless environment. The following video segment explains this critical concept with an excellent animation.

S204 - Azure Durable Functions for serverless .NET orchestration - Jeff Hollan

This section of the video is crucial. It visually demonstrates the event sourcing and replay mechanism that powers Durable Functions, showing how state is persisted in Azure Storage and how the orchestrator function replays its history.

Watch the segment from 18:49 to 24:46. Pay close attention to the interaction between the orchestrator code, the execution history table in storage, and the activity function.


5. Orchestrator Code Constraints

The replay mechanism is powerful, but it imposes strict rules on the code you can write inside an orchestrator function. Because the code is replayed, it must be deterministic. This means that given the same input, it must always follow the exact same logic path and produce the same output.

Failing to follow these constraints will lead to unpredictable behavior and runtime errors.

Watch this final video segment, which details these constraints and how to work around them.

S204 - Azure Durable Functions for serverless .NET orchestration - Jeff Hollan

This segment explains the deterministic code constraints for orchestrator functions and provides the correct, durable-friendly alternatives.

Watch from 33:11 to 36:32. Note down the 'don'ts' (like using DateTime.Now) and the corresponding 'do's' (using context.CurrentUtcDateTime).

Here is a summary of the key rules for orchestrator functions:

Do NOT do this in an orchestrator... Do this instead... Why?
Get the current time (DateTime.Now) Use context.CurrentUtcDateTime The current time will be different on each replay, breaking determinism.
Generate random numbers or GUIDs (new Guid()) Use context.NewGuid() A new GUID will be generated on each replay. context.NewGuid creates one and saves it to history.
Perform I/O (network calls, database access) Move the I/O operations into an Activity Function and call it. I/O operations can have side effects and variable results, which are not deterministic.
Block or sleep (Thread.Sleep) Use context.CreateTimer() This creates a durable timer that safely pauses the orchestration without blocking a thread.
Use static variables Pass data as inputs/outputs to functions, or use Durable Entities (an advanced topic). Static variables are not preserved across replays and can cause unpredictable behavior.

Conclusion

In this lesson, we've laid the conceptual foundation for building stateful serverless workflows. You've moved beyond simple, stateless functions to understand how to orchestrate complex, long-running processes.

Key Takeaways:

  • Durable Functions solve stateful workflow challenges like function chaining, fan-out/fan-in, and waiting for external events.
  • The architecture consists of three main components: Client Functions (starters), Orchestrator Functions (conductors), and Activity Functions (workers).
  • The framework uses an event sourcing and replay mechanism with Azure Storage to maintain state, allowing orchestrators to be both durable and serverless.
  • Because of the replay mechanism, orchestrator code must be deterministic, following a strict set of coding constraints.

Next Steps

Now that you've grasped the architecture and the "rules of the road," it's time to put this knowledge into practice. In the next lesson, you will create a basic Durable Functions orchestration demonstrating the function chaining pattern. We will build, deploy, and run our first stateful workflow.