Durable Functions, at their core, are stateful workflows that run on Azure, and the most surprising thing about them is that they don’t require a dedicated workflow engine service; they leverage existing Azure Storage and Compute resources to provide that statefulness.
Let’s see this in action. Imagine a simple human approval process: a request comes in, an email is sent to an approver, and the workflow waits for a response. Here’s a snippet of what that Durable Function orchestration might look like in C#:
[FunctionName("HumanApprovalWorkflow")]
public static async Task RunAsync(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var requestDetails = context.GetInput<RequestDetails>();
await context.CallActivityAsync("SendApprovalEmail", requestDetails);
DateTime fireAt = context.CurrentUtcDateTime.AddDays(3);
await context.WaitForExternalEventAsync("ApprovalResponse", fireAt);
if (context.HasFiredEvent("ApprovalResponse"))
{
var response = context.GetExternalEvent<ApprovalResponse>("ApprovalResponse");
if (response.Approved)
{
await context.CallActivityAsync("ProcessApprovedRequest", requestDetails);
}
else
{
await context.CallActivityAsync("NotifyRejection", requestDetails);
}
}
else
{
await context.CallActivityAsync("HandleTimeout", requestDetails);
}
}
The OrchestrationTrigger attribute kicks off the workflow. CallActivityAsync invokes a separate Azure Function (an "activity function") to perform a discrete task, like sending an email. The magic happens with WaitForExternalEventAsync. This tells the orchestration to pause, store its current state, and wait for a specific event (like a button click on an approval email) to be sent back to it. The fireAt parameter is crucial here – it sets a timeout, ensuring the workflow doesn’t wait forever if the human never responds.
The problem this solves is managing long-running, stateful processes that involve human interaction or external dependencies without the complexity of managing state yourself. Traditional stateless functions would lose their context upon completion. Durable Functions, by leveraging Azure Table Storage for state and Azure Service Bus for message queuing, can reliably resume execution from where they left off, even after deployments, machine restarts, or extended periods of inactivity.
Internally, the orchestration function itself is stateless. When it’s invoked, it reads its execution history from Azure Table Storage, replays its code, and determines what the next action should be. If the next action is waiting for an external event or a timer, it writes that intention to the history and exits. The Durable Functions extension then handles the actual waiting and re-invoking the orchestration when the event occurs or the timer expires. This replay mechanism is what gives Durable Functions their resilience and allows them to appear stateful.
You control the flow through orchestrator functions written in code. The levers you have are:
- Activities: Discrete units of work.
- Orchestrations: The logic that chains activities, waits for events, and manages state.
- Timers: For time-based delays or timeouts.
- External Events: For receiving signals from external systems or users.
- Durable Entities: For managing fine-grained state for specific entities (e.g., a shopping cart, a user profile).
When you call WaitForExternalEventAsync("ApprovalResponse"), Durable Functions writes an entry to the orchestration’s history indicating that it’s waiting for an event named "ApprovalResponse". It also writes a message to a Service Bus queue that the Durable Functions extension monitors. The external system (or another function) that receives the approval response then calls the Durable Functions API to signal that event, specifying the instance ID of the orchestration it needs to resume. The extension finds the relevant history, replays the orchestration up to the WaitForExternalEventAsync call, sees that the event has now occurred, and proceeds with the execution.
The fireAt parameter in WaitForExternalEventAsync is not just a timeout; it’s also a mechanism for implementing scheduled operations. If you set fireAt to a future date, the orchestration will effectively pause until that time or until the external event arrives, whichever comes first. This is incredibly powerful for scenarios like delayed processing, scheduled reminders, or implementing complex SLA-driven workflows.
The "replay" aspect of Durable Functions, while key to their resilience, can sometimes be a source of confusion if not understood. Because the orchestrator code replays from the beginning on each new event, you must ensure that any code within the orchestrator that has side effects (like making direct HTTP calls or writing to a database) is idempotent or, more commonly, is encapsulated within activity functions. Activity functions are only executed once per history entry, guaranteeing their effects happen exactly once.
The next step in mastering Durable Functions is understanding how to manage state with Durable Entities.