Skip to content

Durable Entities

This recipe covers Durable Entities (the stateful entity model) with Azure Functions Node.js v4 using the durable-functions package. Entity functions require Durable Functions 2.0 or later.

Overview

Durable Entities manage small pieces of explicit state — tiny, addressable, single-threaded objects that live in durable storage. Unlike orchestrator functions, which represent state implicitly through control flow, an entity reads and writes its state explicitly through operations.

Entities are ideal for aggregation (counters, running totals), fan-in of high-volume signals without lock contention, and actor-style per-key state.

Concept Description
Entity function Defines the operations that read and update state. Registered with df.app.entity.
Entity ID A pair of strings: the entity name (e.g. Counter) plus the entity key (the unique instance). Written as @Counter@key.
Operation A named action (for example add, reset, get), with optional input.
Serialized access A single entity processes its operations one at a time, so you never need locks.
flowchart TD
    CLIENT["Client Function\nsignalEntity() / readEntityState()"] -->|"signal (one-way)"| ENT["Entity Function\nCounter@key\noperations run serially"]
    ORC["Orchestrator Function\ncallEntity()"] -->|"call (two-way)"| ENT
    ENT -->|"setState()"| STORE["Durable Store\npersisted entity state"]
    STORE -->|"readEntityState()"| CLIENT

Prerequisites

Install the durable package:

npm install durable-functions

Use extension bundle v4 in host.json:

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}

Define an Entity Function

The entity handler receives a context with df helpers. Read the current state (with a default factory), branch on the operation name, mutate the value, and persist it with setState. Use return to send a value to a two-way caller.

const { app } = require("@azure/functions");
const df = require("durable-functions");

df.app.entity("Counter", function (context) {
  const currentValue = context.df.getState(() => 0);

  switch (context.df.operationName) {
    case "add":
      context.df.setState(currentValue + context.df.getInput());
      break;
    case "reset":
      context.df.setState(0);
      break;
    case "get":
      context.df.return(currentValue);
      break;
  }
});

Signal an Entity from a Client Function

Signaling is one-way (fire-and-forget): the client sends an operation and does not wait for a result.

app.http("addToCounter", {
  methods: ["POST"],
  route: "entities/counter/{key}/add",
  authLevel: "function",
  extraInputs: [df.input.durableClient()],
  handler: async (request, context) => {
    const client = df.getClient(context);
    const key = request.params.key;
    const amount = Number(new URL(request.url).searchParams.get("amount") ?? "1");

    const entityId = new df.EntityId("Counter", key);
    await client.signalEntity(entityId, "add", amount);

    return { status: 202, jsonBody: { entity: `@Counter@${key}`, signaled: "add", amount } };
  }
});

Read Entity State from a Client Function

Reading returns the entity's most recently persisted (committed) state. It may be slightly stale but never reflects a half-completed operation.

app.http("getCounter", {
  methods: ["GET"],
  route: "entities/counter/{key}",
  authLevel: "function",
  extraInputs: [df.input.durableClient()],
  handler: async (request, context) => {
    const client = df.getClient(context);
    const key = request.params.key;
    const entityId = new df.EntityId("Counter", key);

    const state = await client.readEntityState(entityId);

    return {
      status: 200,
      jsonBody: {
        entity: `@Counter@${key}`,
        exists: state.entityExists,
        value: state.entityExists ? state.entityState : 0
      }
    };
  }
});

Call an Entity from an Orchestrator

Orchestrators can call an entity (two-way — wait for a result) to read shared state and make a decision.

df.app.orchestration("reserveSeat", function* (context) {
  const gameId = context.df.getInput();
  const entityId = new df.EntityId("Counter", gameId);

  // Two-way call: read the current seat count and wait for the result.
  const current = yield context.df.callEntity(entityId, "get");

  if (current >= 100) {
    return { reserved: false, reason: "sold out", seatsTaken: current };
  }

  // Claim a seat.
  yield context.df.callEntity(entityId, "add", 1);
  return { reserved: true, seatNumber: current + 1 };
});

Signaling from an orchestrator

In the Node.js model, orchestrators use callEntity (two-way) to communicate with entities. Fire-and-forget signaling is available from client and entity functions.

Access Rules Summary

Caller Signal (one-way) Call (two-way) Read state
Client function Yes No Yes
Orchestrator function Yes No (call get instead)
Entity function Yes No

See Also

Sources