Durable Functions: Advanced Patterns¶
This recipe covers advanced Durable Functions patterns for Node.js (v4 programming model) beyond the basic chaining and fan-out/fan-in flows: sub-orchestrations, eternal orchestrations, activity retries, and safe versioning. For the fundamentals, see Durable Orchestration.
Architecture¶
flowchart TD
PARENT[Parent orchestrator] -->|callSubOrchestrator| SUB1[Sub-orchestrator A]
PARENT -->|callSubOrchestrator| SUB2[Sub-orchestrator B]
SUB1 --> ACT1[Activities]
SUB2 --> ACT2[Activities]
SUB1 --> PARENT
SUB2 --> PARENT Sub-Orchestrations¶
Break a large workflow into reusable orchestrators. A parent calls a sub-orchestrator with context.df.callSubOrchestrator, and can fan out over several the same way it fans out over activities.
const df = require("durable-functions");
df.app.orchestration("parentOrchestrator", function* (context) {
const regions = context.df.getInput().regions;
// Fan out over sub-orchestrations, one per region.
const tasks = regions.map((region) =>
context.df.callSubOrchestrator("processRegion", region)
);
const results = yield context.df.Task.all(tasks);
return { regionsProcessed: results.length, results };
});
df.app.orchestration("processRegion", function* (context) {
const region = context.df.getInput();
const validated = yield context.df.callActivity("validateRegion", region);
return yield context.df.callActivity("loadRegion", validated);
});
Eternal Orchestrations¶
For a workflow that runs indefinitely (aggregators, periodic jobs), do not use an unbounded loop — the history would grow forever. Call context.df.continueAsNew to restart the orchestration with fresh state and a clean history.
const df = require("durable-functions");
const moment = require("moment");
df.app.orchestration("periodicCleanup", function* (context) {
const state = context.df.getInput() || { runs: 0 };
yield context.df.callActivity("runCleanup", state);
state.runs += 1;
// Durable sleep, then restart with new state and empty history.
const nextRun = moment.utc(context.df.currentUtcDateTime).add(1, "h");
yield context.df.createTimer(nextRun.toDate());
context.df.continueAsNew(state);
});
Activity Retries¶
Wrap flaky activities with a retry policy instead of hand-coding retry loops. The orchestration replays cleanly because retries are recorded in history.
const df = require("durable-functions");
df.app.orchestration("resilientOrchestrator", function* (context) {
const order = context.df.getInput();
// firstRetryIntervalInMilliseconds, maxNumberOfAttempts
const retryOptions = new df.RetryOptions(5000, 3);
const result = yield context.df.callActivityWithRetry(
"chargeCustomer", retryOptions, order
);
return result;
});
| Element | Explanation |
|---|---|
callSubOrchestrator | Invokes another orchestrator as a child; compose and fan out like activities. |
continueAsNew | Restarts the orchestration with new input and a trimmed history for eternal loops. |
RetryOptions | Declarative retry policy applied via callActivityWithRetry. |
Versioning¶
Orchestrations replay from history, so changing an orchestrator's code while instances are in flight can break replay (non-determinism). Safe strategies:
- Deploy side by side: give the changed orchestrator a new name and route new instances to it, letting existing instances drain on the old version.
- Do not reorder or remove existing activity calls in a deployed orchestrator.
- Terminate and restart in-flight instances if a breaking change is unavoidable.
Determinism still applies
Advanced patterns do not relax the determinism rule. Never call Date.now(), generate random values, or do direct I/O inside an orchestrator — use activities and context.df.currentUtcDateTime.