Durable Functions: Complexe Workflows Orchestreren in Azure
Hoe Durable Functions langlopende processen, fan-out/fan-in patronen en menselijke goedkeuringsflows mogelijk maakt — met werkende C# voorbeelden.
Jean-Pierre Broeders
Freelance DevOps Engineer
Durable Functions: Complexe Workflows Orchestreren in Azure
Reguliere Azure Functions zijn stateless. Trigger erin, resultaat eruit, klaar. Maar wat als een proces uit tien stappen bestaat, sommige stappen uren duren, en halverwege iemand op een knop moet drukken? Dan vallen gewone functions plat.
Durable Functions lost dat op. Het is een extensie bovenop Azure Functions die state management, retry logic en orchestratie toevoegt. Geen externe queue nodig, geen database om de voortgang bij te houden. De runtime regelt het.
Het Orchestrator Patroon
Een orchestrator function is de dirigent. Die roept andere functions aan in een specifieke volgorde en wacht op resultaten. Klinkt simpel, maar het verschil met een gewone controller is dat de orchestrator kan slapen. Dagen, weken zelfs. Zonder kosten.
[FunctionName("OrderProcessing")]
public static async Task<string> RunOrchestrator(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var order = context.GetInput<Order>();
// Stap 1: Valideer voorraad
var stockOk = await context.CallActivityAsync<bool>(
"CheckStock", order.Items);
if (!stockOk)
return "Out of stock";
// Stap 2: Betaling verwerken
var paymentRef = await context.CallActivityAsync<string>(
"ProcessPayment", order.PaymentDetails);
// Stap 3: Verzending inplannen
await context.CallActivityAsync("ScheduleShipping", new ShippingRequest
{
OrderId = order.Id,
PaymentRef = paymentRef
});
return $"Order {order.Id} afgerond";
}
Elke CallActivityAsync is een aparte function die onafhankelijk draait. Als de orchestrator halverwege crasht, pikt die het weer op waar het gebleven was. Dat werkt door event sourcing onder de motorkap — elke stap wordt opgeslagen in Azure Storage.
Fan-Out/Fan-In: Parallel Werk Verdelen
Een veelvoorkomend scenario: honderd afbeeldingen moeten verwerkt worden na een upload. Eén voor één duurt te lang. Met fan-out start je ze allemaal tegelijk.
[FunctionName("BatchImageProcessor")]
public static async Task RunBatch(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var images = await context.CallActivityAsync<List<string>>(
"GetPendingImages", null);
// Fan-out: alle images parallel verwerken
var tasks = images.Select(img =>
context.CallActivityAsync<ProcessResult>("ResizeImage", img));
// Fan-in: wacht tot alles klaar is
var results = await Task.WhenAll(tasks);
var failed = results.Where(r => !r.Success).ToList();
if (failed.Any())
{
await context.CallActivityAsync("NotifyFailures", failed);
}
}
Dit schaalt automatisch. Azure Functions spint workers op naar behoefte. Bij vijftig afbeeldingen draaien er misschien vijftig instanties tegelijk — en daarna betaal je weer niks.
Wel opletten: er zit een limiet op het aantal parallelle executions per function app. In het consumption plan is dat standaard 200. Voor grotere batches is het verstandig om chunking toe te passen.
Menselijke Goedkeuring in een Workflow
Soms moet een proces wachten op een mens. Een manager die een bestelling goedkeurt, een klant die een e-mailadres bevestigt. Durable Functions heeft daar external events voor.
[FunctionName("ApprovalWorkflow")]
public static async Task<string> RunApproval(
[OrchestrationTrigger] IDurableOrchestrationContext context)
{
var request = context.GetInput<ApprovalRequest>();
await context.CallActivityAsync("SendApprovalEmail", request);
// Wacht maximaal 72 uur op goedkeuring
using var cts = new CancellationTokenSource();
var approvalTask = context.WaitForExternalEvent<bool>("ApprovalResult");
var timeoutTask = context.CreateTimer(
context.CurrentUtcDateTime.AddHours(72), cts.Token);
var winner = await Task.WhenAny(approvalTask, timeoutTask);
if (winner == approvalTask)
{
cts.Cancel();
return approvalTask.Result ? "Goedgekeurd" : "Afgewezen";
}
return "Verlopen — geen reactie binnen 72 uur";
}
De function slaapt letterlijk tot dat event binnenkomt. Geen polling, geen cron job die elke minuut checkt. Het externe systeem (een API endpoint, een webhook) stuurt een event naar de orchestrator en die gaat verder.
Retry Policies: Niet Alles Lukt de Eerste Keer
Externe API's falen. Dat is geen vraag van óf, maar wanneer. Durable Functions heeft ingebouwde retry-opties.
var retryOptions = new RetryOptions(
firstRetryInterval: TimeSpan.FromSeconds(5),
maxNumberOfAttempts: 3)
{
BackoffCoefficient = 2.0,
MaxRetryInterval = TimeSpan.FromMinutes(1)
};
await context.CallActivityWithRetryAsync(
"CallExternalApi", retryOptions, requestData);
Exponential backoff met een maximum. Geen custom code nodig, geen Polly wrapper eromheen. Het zit er gewoon in.
Valkuilen
Een paar dingen die niet direct duidelijk zijn uit de documentatie:
Orchestrators moeten deterministisch zijn. Geen DateTime.Now, geen Guid.NewGuid(), geen random calls. Gebruik altijd context.CurrentUtcDateTime en context.NewGuid(). De replay-mechanisme vereist dat de code elke keer hetzelfde pad volgt.
Grote payloads vermijden. Alles wat tussen activities en orchestrators gaat, wordt geserialiseerd naar Azure Storage. Een 50MB object doorgeven als parameter is een recept voor trage uitvoering en hoge storage kosten. Geef references door, geen data.
Monitoring niet vergeten. Application Insights integratie is er, maar moet expliciet geconfigureerd worden. Zonder monitoring is debuggen van een langlopende orchestratie pure ellende.
Wanneer Wel, Wanneer Niet
Durable Functions past goed bij orderverwerking, goedkeuringsflows, batch processing en ETL-pipelines. Het past slecht bij simpele request-response API's of real-time verwerking waar latency kritiek is. De overhead van de orchestratie-laag voegt milliseconden toe die er bij een simpele HTTP trigger niet hoeven te zijn.
Het is een gereedschap, geen silver bullet. Maar voor de juiste use case bespaart het weken aan zelfgebouwde state machines en queue-consumers.
