Durable Entities¶
This recipe covers Durable Entities (the stateful entity model) with Azure Functions Java. Durable Functions for Java supports entities using a class-based syntax from version 1.9.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 class | Extends AbstractTaskEntity<TState>; each public method is an operation. |
| Entity ID | An EntityInstanceId pair: the entity name (e.g. Counter) plus the entity key (the unique instance). |
| Operation | A named method invoked by name (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() / getEntityMetadata()"] -->|"signal (one-way)"| ENT["Entity Function\nCounter@key\noperations run serially"]
ORC["Orchestrator Function\ncallEntity() / signalEntity()"] -->|"call (two-way)"| ENT
ENT -->|"state persisted"| STORE["Durable Store\npersisted entity state"]
STORE -->|"readStateAs()"| CLIENT Prerequisites¶
Ensure your project references Durable Functions for Java 1.9.0 or later. Entities persist state in Azure Storage (AzureWebJobsStorage), like orchestrations.
Define an Entity Class¶
Extend AbstractTaskEntity<TState>. Each public method is an operation; initializeState supplies the default state.
package com.contoso.functions;
import com.microsoft.durabletask.AbstractTaskEntity;
import com.microsoft.durabletask.TaskEntityOperation;
public class CounterEntity extends AbstractTaskEntity<Integer> {
public void add(int value) {
this.state += value;
}
public void subtract(int value) {
this.state -= value;
}
public int get() {
return this.state;
}
public void reset() {
this.state = 0;
}
@Override
protected Integer initializeState(TaskEntityOperation operation) {
return 0;
}
@Override
protected Class<Integer> getStateType() {
return Integer.class;
}
}
Bind the entity class to an entity trigger function:
import com.microsoft.azure.functions.annotation.*;
import com.microsoft.durabletask.azurefunctions.DurableEntityTrigger;
import com.microsoft.durabletask.EntityRunner;
public class EntityFunctions {
@FunctionName("Counter")
public String counterEntity(
@DurableEntityTrigger(name = "req") String req) {
return EntityRunner.loadAndRun(req, CounterEntity::new);
}
}
Signal an Entity from a Client Function¶
Signaling is one-way (fire-and-forget). Use the DurableClientContext.
@FunctionName("SignalCounter")
public HttpResponseMessage signalCounter(
@HttpTrigger(name = "req", methods = {HttpMethod.POST},
authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Void> request,
@DurableClientInput(name = "durableContext") DurableClientContext durableContext) {
String key = request.getQueryParameters().getOrDefault("key", "myCounter");
int value = Integer.parseInt(request.getQueryParameters().getOrDefault("value", "1"));
EntityInstanceId entityId = new EntityInstanceId("Counter", key);
durableContext.signalEntity(entityId, "add", value);
return request.createResponseBuilder(HttpStatus.ACCEPTED)
.body("Signaled add on entity '" + key + "'")
.build();
}
Read Entity State from a Client Function¶
Reading returns the entity's most recently persisted (committed) state.
@FunctionName("GetCounter")
public HttpResponseMessage getCounter(
@HttpTrigger(name = "req", methods = {HttpMethod.GET},
authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Void> request,
@DurableClientInput(name = "durableContext") DurableClientContext durableContext) {
String key = request.getQueryParameters().getOrDefault("key", "myCounter");
EntityInstanceId entityId = new EntityInstanceId("Counter", key);
EntityMetadata metadata = durableContext.getEntityMetadata(entityId, true);
if (metadata == null) {
return request.createResponseBuilder(HttpStatus.NOT_FOUND)
.body("Entity '" + key + "' not found").build();
}
Integer state = metadata.readStateAs(Integer.class);
return request.createResponseBuilder(HttpStatus.OK)
.header("Content-Type", "application/json")
.body("{\"key\": \"" + key + "\", \"value\": " + state + "}")
.build();
}
Call and Signal an Entity from an Orchestrator¶
Orchestrators can both signal (one-way) and call (two-way) entities.
@FunctionName("CounterOrchestration")
public String counterOrchestration(
@DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
String key = ctx.getInput(String.class);
EntityInstanceId entityId = new EntityInstanceId("Counter", key);
// Fire-and-forget signals.
ctx.signalEntity(entityId, "add", 10);
ctx.signalEntity(entityId, "subtract", 3);
// Two-way call: wait for the result.
int value = ctx.callEntity(entityId, "get", Integer.class).await();
return "Counter '" + key + "' final value: " + value;
}
Access Rules Summary¶
| Caller | Signal (one-way) | Call (two-way) | Read state |
|---|---|---|---|
| Client function | Yes | No | Yes |
| Orchestrator function | Yes | Yes | No (call get instead) |
| Entity function | Yes | No | — |