diff --git a/samples/durable-functions/java/Entities/README.md b/samples/durable-functions/java/Entities/README.md
new file mode 100644
index 0000000..8a9fc0f
--- /dev/null
+++ b/samples/durable-functions/java/Entities/README.md
@@ -0,0 +1,65 @@
+# Durable Entities — Durable Functions Java Sample
+
+Java | Durable Functions
+
+## Description
+
+This sample demonstrates Durable Entities with Java using the Durable Task Scheduler backend. It includes:
+
+1. A `Counter` durable entity with `add`, `subtract`, `get`, and `reset` operations
+2. An HTTP endpoint for direct entity signals
+3. An HTTP endpoint for direct entity reads
+4. An orchestration that signals the entity and reads its final value
+
+## Prerequisites
+
+1. [Java 11+](https://adoptium.net/) (JDK)
+2. [Maven](https://maven.apache.org/download.cgi)
+3. [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local)
+4. [Docker](https://www.docker.com/products/docker-desktop/) (for the DTS emulator and Azurite)
+
+## Quick Run
+
+1. Start the Durable Task Scheduler emulator:
+ ```bash
+ docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
+ ```
+
+2. Start Azurite for Azure Functions host storage:
+ ```bash
+ docker run --name azurite -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
+ ```
+
+3. Build the sample:
+ ```bash
+ cd samples/durable-functions/java/Entities
+ mvn clean package
+ ```
+
+4. Run the Functions host:
+ ```bash
+ mvn azure-functions:run
+ ```
+
+5. Signal the entity directly:
+ ```bash
+ curl -X POST "http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=7"
+ ```
+
+6. Read the entity state:
+ ```bash
+ curl "http://localhost:7071/api/GetCounter?key=my-counter"
+ ```
+
+7. Start the orchestration:
+ ```bash
+ curl -X POST "http://localhost:7071/api/StartCounterOrchestration?key=my-orch-counter"
+ ```
+
+8. View orchestration activity in the dashboard: http://localhost:8082
+
+## Notes
+
+- `mvn clean package` is configured to stage the Azure Functions app so `mvn azure-functions:run` works as a separate second step.
+- `AzureWebJobsStorage=UseDevelopmentStorage=true` requires Azurite to be running locally.
+- `SignalCounter` rejects `op=get`; use `GetCounter` to read entity state.
\ No newline at end of file
diff --git a/samples/durable-functions/java/Entities/demo.http b/samples/durable-functions/java/Entities/demo.http
new file mode 100644
index 0000000..bffa36b
--- /dev/null
+++ b/samples/durable-functions/java/Entities/demo.http
@@ -0,0 +1,27 @@
+# For more info on HTTP files go to https://aka.ms/vs/httpfile
+
+### Signal the counter entity: add 10
+POST http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=10
+
+### Signal the counter entity: add 5
+POST http://localhost:7071/api/SignalCounter?key=my-counter&op=add&value=5
+
+### Signal the counter entity: subtract 3
+POST http://localhost:7071/api/SignalCounter?key=my-counter&op=subtract&value=3
+
+### Signal the counter entity: reset
+POST http://localhost:7071/api/SignalCounter?key=my-counter&op=reset
+
+### Get the current counter value
+GET http://localhost:7071/api/GetCounter?key=my-counter
+
+### Start the counter orchestration
+POST http://localhost:7071/api/StartCounterOrchestration?key=my-orch-counter
+
+### Check orchestration status
+# Copy the statusQueryGetUri from the StartCounterOrchestration response above
+# and paste it below (it includes the required system key):
+GET http://localhost:7071/runtime/webhooks/durabletask/instances/{instanceId}?code={systemKey}
+
+### Get the orchestration-managed counter value
+GET http://localhost:7071/api/GetCounter?key=my-orch-counter
diff --git a/samples/durable-functions/java/Entities/host.json b/samples/durable-functions/java/Entities/host.json
new file mode 100644
index 0000000..431660d
--- /dev/null
+++ b/samples/durable-functions/java/Entities/host.json
@@ -0,0 +1,21 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "DurableTask.Core": "Warning"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "azureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ },
+ "extensionBundle": {
+ "id": "Microsoft.Azure.Functions.ExtensionBundle",
+ "version": "[4.*, 5.0.0)"
+ }
+}
diff --git a/samples/durable-functions/java/Entities/local.settings.json b/samples/durable-functions/java/Entities/local.settings.json
new file mode 100644
index 0000000..89a4b63
--- /dev/null
+++ b/samples/durable-functions/java/Entities/local.settings.json
@@ -0,0 +1,8 @@
+{
+ "IsEncrypted": false,
+ "Values": {
+ "AzureWebJobsStorage": "UseDevelopmentStorage=true",
+ "FUNCTIONS_WORKER_RUNTIME": "java",
+ "DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
+ }
+}
diff --git a/samples/durable-functions/java/Entities/pom.xml b/samples/durable-functions/java/Entities/pom.xml
new file mode 100644
index 0000000..d034fc4
--- /dev/null
+++ b/samples/durable-functions/java/Entities/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+ com.example
+ durable-functions-entities
+ 1.0-SNAPSHOT
+ jar
+
+
+ UTF-8
+ 11
+ 1.41.0
+ 3.2.4
+ 1.9.0
+
+
+
+
+ com.microsoft.azure.functions
+ azure-functions-java-library
+ ${azure.functions.java.library.version}
+
+
+ com.microsoft
+ durabletask-azure-functions
+ ${durabletask.azure.functions.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.15.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+ com.microsoft.azure
+ azure-functions-maven-plugin
+ ${azure.functions.maven.plugin.version}
+
+
+ package-functions
+
+ package
+
+
+
+
+ durable-functions-entities
+ java-functions-group
+ java-functions-app-service-plan
+ westus2
+
+ linux
+ 11
+
+
+
+
+
+
diff --git a/samples/durable-functions/java/Entities/src/main/java/com/example/CounterEntity.java b/samples/durable-functions/java/Entities/src/main/java/com/example/CounterEntity.java
new file mode 100644
index 0000000..0f25701
--- /dev/null
+++ b/samples/durable-functions/java/Entities/src/main/java/com/example/CounterEntity.java
@@ -0,0 +1,50 @@
+package com.example;
+
+import com.microsoft.durabletask.AbstractTaskEntity;
+import com.microsoft.durabletask.TaskEntityOperation;
+
+import java.util.logging.Logger;
+
+/**
+ * A durable entity that maintains a counter state.
+ *
+ * Supports operations: add, subtract, get, reset.
+ * Public methods are automatically dispatched based on operation name.
+ */
+public class CounterEntity extends AbstractTaskEntity {
+ private static final Logger logger = Logger.getLogger(CounterEntity.class.getName());
+
+ @Override
+ protected Integer initializeState(TaskEntityOperation operation) {
+ return 0;
+ }
+
+ @Override
+ protected Class getStateType() {
+ return Integer.class;
+ }
+
+ public void add(int value) {
+ this.state += value;
+ logger.info(String.format("Counter '%s': Added %d, new value: %d",
+ this.context.getId().getKey(), value, this.state));
+ }
+
+ public void subtract(int value) {
+ this.state -= value;
+ logger.info(String.format("Counter '%s': Subtracted %d, new value: %d",
+ this.context.getId().getKey(), value, this.state));
+ }
+
+ public int get() {
+ logger.info(String.format("Counter '%s': Current value: %d",
+ this.context.getId().getKey(), this.state));
+ return this.state;
+ }
+
+ public void reset() {
+ this.state = 0;
+ logger.info(String.format("Counter '%s': Reset to 0",
+ this.context.getId().getKey()));
+ }
+}
diff --git a/samples/durable-functions/java/Entities/src/main/java/com/example/Functions.java b/samples/durable-functions/java/Entities/src/main/java/com/example/Functions.java
new file mode 100644
index 0000000..5936ddc
--- /dev/null
+++ b/samples/durable-functions/java/Entities/src/main/java/com/example/Functions.java
@@ -0,0 +1,136 @@
+package com.example;
+
+import com.microsoft.azure.functions.*;
+import com.microsoft.azure.functions.annotation.*;
+import com.microsoft.durabletask.*;
+import com.microsoft.durabletask.azurefunctions.*;
+
+/**
+ * Azure Functions with Durable Entities.
+ *
+ * This sample demonstrates:
+ * 1. A counter entity that supports add, subtract, get, and reset operations
+ * 2. An orchestration that signals and calls the counter entity
+ * 3. HTTP triggers to start orchestrations and signal entities directly
+ */
+public class Functions {
+ private static final String DEFAULT_ENTITY_KEY = "my-counter";
+
+
+ /**
+ * Entity function for the counter entity.
+ * The @DurableEntityTrigger binds to incoming entity operation requests.
+ */
+ @FunctionName("Counter")
+ public String counterEntity(
+ @DurableEntityTrigger(name = "req", entityName = "Counter") String req) {
+ return EntityRunner.loadAndRun(req, CounterEntity::new);
+ }
+
+ /**
+ * Orchestration that interacts with the counter entity.
+ * Signals add/subtract operations and calls get to retrieve the value.
+ */
+ @FunctionName("CounterOrchestration")
+ public String counterOrchestration(
+ @DurableOrchestrationTrigger(name = "ctx") TaskOrchestrationContext ctx) {
+
+ String entityKey = ctx.getInput(String.class);
+ if (entityKey == null || entityKey.isBlank()) {
+ entityKey = DEFAULT_ENTITY_KEY;
+ }
+ EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey);
+
+ // Signal entity operations (fire-and-forget)
+ ctx.signalEntity(entityId, "add", 10);
+ ctx.signalEntity(entityId, "add", 5);
+ ctx.signalEntity(entityId, "subtract", 3);
+
+ // Call entity and wait for result
+ int value = ctx.callEntity(entityId, "get", Integer.class).await();
+
+ return "Counter '" + entityKey + "' final value: " + value;
+ }
+
+ /**
+ * HTTP trigger that starts the counter orchestration.
+ * POST /api/StartCounterOrchestration?key=my-counter
+ */
+ @FunctionName("StartCounterOrchestration")
+ public HttpResponseMessage startCounterOrchestration(
+ @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS)
+ HttpRequestMessage request,
+ @DurableClientInput(name = "durableContext") DurableClientContext durableContext) {
+
+ DurableTaskClient client = durableContext.getClient();
+ String entityKey = request.getQueryParameters().getOrDefault("key", DEFAULT_ENTITY_KEY);
+
+ String instanceId = client.scheduleNewOrchestrationInstance(
+ "CounterOrchestration",
+ new NewOrchestrationInstanceOptions().setInput(entityKey));
+
+ return durableContext.createCheckStatusResponse(request, instanceId);
+ }
+
+ /**
+ * HTTP trigger that signals the counter entity directly.
+ * POST /api/SignalCounter?key=my-counter&op=add&value=10
+ */
+ @FunctionName("SignalCounter")
+ public HttpResponseMessage signalCounter(
+ @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS)
+ HttpRequestMessage request,
+ @DurableClientInput(name = "durableContext") DurableClientContext durableContext) {
+
+ String entityKey = request.getQueryParameters().getOrDefault("key", DEFAULT_ENTITY_KEY);
+ String operation = request.getQueryParameters().getOrDefault("op", "add");
+ String valueStr = request.getQueryParameters().getOrDefault("value", "1");
+
+ EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey);
+
+ if ("get".equals(operation)) {
+ return request.createResponseBuilder(HttpStatus.BAD_REQUEST)
+ .body("Use GET /api/GetCounter?key= to read entity state.")
+ .build();
+ }
+
+ if ("reset".equals(operation)) {
+ durableContext.signalEntity(entityId, operation);
+ } else {
+ int value = Integer.parseInt(valueStr);
+ durableContext.signalEntity(entityId, operation, value);
+ }
+
+ return request.createResponseBuilder(HttpStatus.ACCEPTED)
+ .body("Signal sent: " + operation + " on entity '" + entityKey + "'")
+ .build();
+ }
+
+ /**
+ * HTTP trigger that gets the current state of a counter entity.
+ * GET /api/GetCounter?key=my-counter
+ */
+ @FunctionName("GetCounter")
+ public HttpResponseMessage getCounter(
+ @HttpTrigger(name = "req", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS)
+ HttpRequestMessage request,
+ @DurableClientInput(name = "durableContext") DurableClientContext durableContext) {
+
+ String entityKey = request.getQueryParameters().getOrDefault("key", DEFAULT_ENTITY_KEY);
+ EntityInstanceId entityId = new EntityInstanceId("Counter", entityKey);
+
+ EntityMetadata metadata = durableContext.getEntityMetadata(entityId, true);
+
+ if (metadata == null) {
+ return request.createResponseBuilder(HttpStatus.NOT_FOUND)
+ .body("Entity '" + entityKey + "' not found")
+ .build();
+ }
+
+ Integer state = metadata.readStateAs(Integer.class);
+ return request.createResponseBuilder(HttpStatus.OK)
+ .header("Content-Type", "application/json")
+ .body("{\"key\": \"" + entityKey + "\", \"value\": " + state + "}")
+ .build();
+ }
+}
diff --git a/samples/durable-functions/java/HelloCities/README.md b/samples/durable-functions/java/HelloCities/README.md
index 35228cb..4bd1a39 100644
--- a/samples/durable-functions/java/HelloCities/README.md
+++ b/samples/durable-functions/java/HelloCities/README.md
@@ -14,33 +14,48 @@ This quickstart demonstrates Durable Functions with Java using the Durable Task
1. [Java 11+](https://adoptium.net/) (JDK)
2. [Maven](https://maven.apache.org/download.cgi)
3. [Azure Functions Core Tools v4](https://learn.microsoft.com/azure/azure-functions/functions-run-local)
-4. [Docker](https://www.docker.com/products/docker-desktop/) (for the emulator)
+4. [Docker](https://www.docker.com/products/docker-desktop/) (for the DTS emulator and Azurite)
## Quick Run
-1. Start the emulator:
+1. Start the Durable Task Scheduler emulator:
```bash
- docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
+ docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
```
-2. Build and run:
+2. Start Azurite for Azure Functions host storage:
+ ```bash
+ docker run --name azurite -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
+ ```
+
+3. Build the sample:
```bash
cd samples/durable-functions/java/HelloCities
mvn clean package
- func start
```
-3. Trigger the function chaining orchestration:
+4. Run the Functions host:
+ ```bash
+ mvn azure-functions:run
+ ```
+
+5. Trigger the function chaining orchestration:
```bash
curl -X POST http://localhost:7071/api/StartChaining
```
-4. Trigger the fan-out/fan-in orchestration:
+6. Trigger the fan-out/fan-in orchestration:
```bash
curl -X POST http://localhost:7071/api/StartFanOutFanIn
```
-5. View in the dashboard: http://localhost:8082
+7. View in the dashboard: http://localhost:8082
+
+## Notes
+
+- `mvn clean package` is configured to stage the Azure Functions app so `mvn azure-functions:run` works as a separate second step.
+- `AzureWebJobsStorage=UseDevelopmentStorage=true` requires Azurite to be running locally.
+- `DURABLE_TASK_SCHEDULER_CONNECTION_STRING` in `local.settings.json` points to the local DTS emulator on `http://localhost:8080`.
## Expected Output
diff --git a/samples/durable-functions/java/HelloCities/host.json b/samples/durable-functions/java/HelloCities/host.json
index 4020661..431660d 100644
--- a/samples/durable-functions/java/HelloCities/host.json
+++ b/samples/durable-functions/java/HelloCities/host.json
@@ -9,7 +9,7 @@
"durableTask": {
"hubName": "default",
"storageProvider": {
- "type": "durabletask-scheduler",
+ "type": "azureManaged",
"connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
}
}
diff --git a/samples/durable-functions/java/HelloCities/pom.xml b/samples/durable-functions/java/HelloCities/pom.xml
index c0431f9..2e64d41 100644
--- a/samples/durable-functions/java/HelloCities/pom.xml
+++ b/samples/durable-functions/java/HelloCities/pom.xml
@@ -45,6 +45,14 @@
com.microsoft.azure
azure-functions-maven-plugin
${azure.functions.maven.plugin.version}
+
+
+ package-functions
+
+ package
+
+
+
durable-functions-hello-cities
java-functions-group
diff --git a/samples/durable-functions/java/README.md b/samples/durable-functions/java/README.md
index 8c52853..e13c9f7 100644
--- a/samples/durable-functions/java/README.md
+++ b/samples/durable-functions/java/README.md
@@ -3,3 +3,26 @@
[Durable Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview) is an extension of Azure Functions that lets you write stateful functions in a serverless compute environment.
These samples demonstrate how to use Durable Functions with Java and the Durable Task Scheduler backend.
+
+## Available Samples
+
+- **HelloCities**: Function chaining and fan-out/fan-in patterns
+- **Entities**: Durable entities with a counter example (add, subtract, get, reset operations)
+
+## Local Run Model
+
+Before running either Java sample locally, start the required services:
+
+```bash
+docker run --name dtsemulator -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
+docker run --name azurite -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite
+```
+
+Then run the sample using the same two Maven commands as the Java Durable Functions quickstart:
+
+```bash
+mvn clean package
+mvn azure-functions:run
+```
+
+`mvn clean package` stages the Azure Functions app, and `mvn azure-functions:run` starts the local Functions host from that staged output.
diff --git a/samples/durable-task-sdks/java/README.md b/samples/durable-task-sdks/java/README.md
index 045db62..17672f1 100644
--- a/samples/durable-task-sdks/java/README.md
+++ b/samples/durable-task-sdks/java/README.md
@@ -41,6 +41,7 @@ Each sample demonstrates a different orchestration pattern:
- **human-interaction**: Integration of human approval steps in orchestrations
- **monitoring**: Monitoring and tracking orchestration progress
- **sub-orchestrations**: Composing multiple orchestrations hierarchically
+- **entities**: Stateful entities that maintain state across operations (counter example)
## Running the Samples
@@ -77,6 +78,10 @@ cd monitoring
# For sub-orchestrations sample
cd sub-orchestrations
./gradlew runSubOrchestrationPattern
+
+# For entities sample
+cd entities
+./gradlew runEntitiesPattern
```
## Testing
diff --git a/samples/durable-task-sdks/java/entities/Dockerfile b/samples/durable-task-sdks/java/entities/Dockerfile
new file mode 100644
index 0000000..154aba8
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/Dockerfile
@@ -0,0 +1,15 @@
+FROM eclipse-temurin:25-jdk
+
+# Install dos2unix to fix line endings
+RUN apt-get update && apt-get install -y dos2unix && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Copy all files
+COPY . .
+
+# Fix line endings for gradlew
+RUN dos2unix gradlew && chmod +x gradlew
+
+# Run the gradle task
+CMD ["./gradlew", "runEntitiesPattern"]
diff --git a/samples/durable-task-sdks/java/entities/azure.yaml b/samples/durable-task-sdks/java/entities/azure.yaml
new file mode 100644
index 0000000..b71b345
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/azure.yaml
@@ -0,0 +1,15 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+
+metadata:
+ template: entities-java
+name: dts-entities
+infra:
+ path: ../../../infra
+services:
+ sampleapp:
+ project: .
+ language: java
+ host: containerapp
+ apiVersion: 2025-01-01
+ docker:
+ path: ./Dockerfile
diff --git a/samples/durable-task-sdks/java/entities/build.gradle b/samples/durable-task-sdks/java/entities/build.gradle
new file mode 100644
index 0000000..9e42cc5
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/build.gradle
@@ -0,0 +1,40 @@
+plugins {
+ id 'java'
+ id 'application'
+}
+
+group 'io.durabletask'
+version = '0.1.0'
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+}
+def grpcVersion = '1.80.0'
+base { archivesName = 'durabletask-samples' }
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+}
+
+task runEntitiesPattern(type: JavaExec) {
+ classpath = sourceSets.main.runtimeClasspath
+ mainClass = 'io.durabletask.samples.EntitiesPattern'
+ systemProperty 'logback.configurationFile', 'src/main/resources/logback-spring.xml'
+}
+
+dependencies {
+ implementation("com.microsoft:durabletask-client:1.9.0")
+ implementation("com.microsoft:durabletask-azuremanaged:1.9.0")
+
+ // Logging dependencies
+ implementation 'ch.qos.logback:logback-classic:1.5.32'
+ implementation 'org.slf4j:slf4j-api:2.0.17'
+
+ // https://github.com/grpc/grpc-java#download
+ implementation "io.grpc:grpc-protobuf:${grpcVersion}"
+ implementation "io.grpc:grpc-stub:${grpcVersion}"
+ runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
+ implementation 'com.azure:azure-identity:1.18.2'
+}
diff --git a/samples/durable-task-sdks/java/entities/gradle/wrapper/gradle-wrapper.properties b/samples/durable-task-sdks/java/entities/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3499ded
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/samples/durable-task-sdks/java/entities/gradlew b/samples/durable-task-sdks/java/entities/gradlew
new file mode 100644
index 0000000..0262dcb
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/samples/durable-task-sdks/java/entities/gradlew.bat b/samples/durable-task-sdks/java/entities/gradlew.bat
new file mode 100644
index 0000000..c4bdd3a
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/gradlew.bat
@@ -0,0 +1,93 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/samples/durable-task-sdks/java/entities/settings.gradle b/samples/durable-task-sdks/java/entities/settings.gradle
new file mode 100644
index 0000000..03ebdfe
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'durabletask-entities-sample'
diff --git a/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/CounterEntity.java b/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/CounterEntity.java
new file mode 100644
index 0000000..203c54b
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/CounterEntity.java
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package io.durabletask.samples;
+
+import com.microsoft.durabletask.AbstractTaskEntity;
+import com.microsoft.durabletask.TaskEntityOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A durable entity that maintains a counter state.
+ *
+ * Supports operations: add, subtract, get, reset.
+ * Public methods are automatically dispatched based on operation name.
+ */
+public class CounterEntity extends AbstractTaskEntity {
+ private static final Logger logger = LoggerFactory.getLogger(CounterEntity.class);
+
+ @Override
+ protected Integer initializeState(TaskEntityOperation operation) {
+ return 0;
+ }
+
+ @Override
+ protected Class getStateType() {
+ return Integer.class;
+ }
+
+ public void add(int value) {
+ this.state += value;
+ logger.info("Counter '{}': Added {}, new value: {}", this.context.getId().getKey(), value, this.state);
+ }
+
+ public void subtract(int value) {
+ this.state -= value;
+ logger.info("Counter '{}': Subtracted {}, new value: {}", this.context.getId().getKey(), value, this.state);
+ }
+
+ public int get() {
+ logger.info("Counter '{}': Current value: {}", this.context.getId().getKey(), this.state);
+ return this.state;
+ }
+
+ public void reset() {
+ this.state = 0;
+ logger.info("Counter '{}': Reset to 0", this.context.getId().getKey());
+ }
+}
diff --git a/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/EntitiesPattern.java b/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/EntitiesPattern.java
new file mode 100644
index 0000000..7c85f10
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/src/main/java/io/durabletask/samples/EntitiesPattern.java
@@ -0,0 +1,184 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+package io.durabletask.samples;
+
+import com.microsoft.durabletask.*;
+import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions;
+import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Objects;
+import java.util.concurrent.TimeoutException;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenRequestContext;
+import com.azure.core.credential.TokenCredential;
+import com.azure.identity.*;
+
+/**
+ * Demonstrates the Durable Entities pattern using the Java Durable Task SDK.
+ *
+ * This sample:
+ * 1. Registers a counter entity that supports add, subtract, get, and reset operations
+ * 2. Registers an orchestration that interacts with the counter entity
+ * 3. Signals the entity directly from the client
+ * 4. Runs an orchestration that signals and calls the entity
+ */
+final class EntitiesPattern {
+ private static final Logger logger = LoggerFactory.getLogger(EntitiesPattern.class);
+ private static final String DEFAULT_ENTITY_KEY = "my-counter";
+
+ public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
+ // Get environment variables for endpoint and taskhub with defaults
+ String endpoint = System.getenv("ENDPOINT");
+ String taskHubName = System.getenv("TASKHUB");
+ String connectionString = System.getenv("DURABLE_TASK_CONNECTION_STRING");
+
+ if (connectionString == null) {
+ if (endpoint != null && taskHubName != null) {
+ String hostAddress = endpoint;
+ if (endpoint.contains(";")) {
+ hostAddress = endpoint.split(";")[0];
+ }
+
+ boolean isLocalEmulator = endpoint.equals("http://localhost:8080");
+
+ if (isLocalEmulator) {
+ connectionString = String.format("Endpoint=%s;TaskHub=%s;Authentication=None", hostAddress, taskHubName);
+ logger.info("Using local emulator with no authentication");
+ } else {
+ connectionString = String.format("Endpoint=%s;TaskHub=%s;Authentication=DefaultAzure", hostAddress, taskHubName);
+ logger.info("Using Azure endpoint with DefaultAzure authentication");
+ }
+
+ logger.info("Using endpoint: {}", endpoint);
+ logger.info("Using task hub: {}", taskHubName);
+ } else {
+ connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
+ logger.info("Using default local emulator connection string");
+ }
+ }
+
+ // Check if we're running in Azure with a managed identity
+ String clientId = System.getenv("AZURE_MANAGED_IDENTITY_CLIENT_ID");
+ TokenCredential credential = null;
+ if (clientId != null && !clientId.isEmpty()) {
+ logger.info("Using Managed Identity with client ID: {}", clientId);
+ credential = new ManagedIdentityCredentialBuilder().clientId(clientId).build();
+
+ AccessToken token = credential.getToken(
+ new TokenRequestContext().addScopes("https://management.azure.com/.default"))
+ .block(Duration.ofSeconds(10));
+ logger.info("Successfully authenticated with Managed Identity, expires at {}", token.getExpiresAt());
+
+ } else if (!connectionString.contains("Authentication=None")) {
+ logger.info("No Managed Identity client ID found, using DefaultAzure authentication");
+ }
+
+ // Create worker using Azure-managed extensions
+ DurableTaskGrpcWorker worker = (credential != null
+ ? DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(endpoint, taskHubName, credential)
+ : DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(connectionString))
+ // Register the counter entity
+ .addEntity("counter", CounterEntity::new)
+ // Register the orchestration that interacts with the entity
+ .addOrchestration(new TaskOrchestrationFactory() {
+ @Override
+ public String getName() { return "CounterWorkflow"; }
+
+ @Override
+ public TaskOrchestration create() {
+ return ctx -> {
+ String entityKey = ctx.getInput(String.class);
+ if (entityKey == null || entityKey.isBlank()) {
+ entityKey = DEFAULT_ENTITY_KEY;
+ }
+ EntityInstanceId entityId = new EntityInstanceId("counter", Objects.requireNonNull(entityKey));
+
+ // Signal entity operations (fire-and-forget)
+ ctx.signalEntity(entityId, "add", 10);
+ ctx.signalEntity(entityId, "add", 5);
+ ctx.signalEntity(entityId, "subtract", 3);
+
+ // Call entity and wait for result
+ int value = ctx.callEntity(entityId, "get", Integer.class).await();
+
+ ctx.complete("Counter '" + entityKey + "' final value: " + value);
+ };
+ }
+ })
+ .build();
+
+ // Start the worker and wait for it to connect
+ worker.start();
+ Thread.sleep(5000);
+
+ // Create client using Azure-managed extensions
+ DurableTaskClient client = (credential != null
+ ? DurableTaskSchedulerClientExtensions.createClientBuilder(endpoint, taskHubName, credential)
+ : DurableTaskSchedulerClientExtensions.createClientBuilder(connectionString)).build();
+
+ String entityKey = DEFAULT_ENTITY_KEY;
+ if (args.length > 0 && args[0] != null && !args[0].isBlank()) {
+ entityKey = args[0];
+ }
+
+ DurableEntityClient entityClient = client.getEntities();
+
+ // Demonstrate direct entity signaling from client
+ logger.info("=== Direct Entity Operations ===");
+ EntityInstanceId entityId = new EntityInstanceId("counter", Objects.requireNonNull(entityKey));
+
+ logger.info("Signaling entity '{}' to add 100", entityKey);
+ entityClient.signalEntity(entityId, "add", 100);
+ Thread.sleep(2000);
+
+ logger.info("Signaling entity '{}' to subtract 25", entityKey);
+ entityClient.signalEntity(entityId, "subtract", 25);
+ Thread.sleep(2000);
+
+ // Run orchestrations that interact with entities
+ logger.info("=== Orchestration-based Entity Operations ===");
+ int totalOrchestrations = 5;
+ int completedOrchestrations = 0;
+ int failedOrTimedOutOrchestrations = 0;
+
+ for (int i = 0; i < totalOrchestrations; i++) {
+ String instanceEntityKey = entityKey + "-orch-" + (i + 1);
+ logger.info("Scheduling orchestration #{} for entity '{}'", i + 1, instanceEntityKey);
+
+ String instanceId = client.scheduleNewOrchestrationInstance(
+ "CounterWorkflow",
+ new NewOrchestrationInstanceOptions().setInput(instanceEntityKey));
+ logger.info("Orchestration #{} scheduled with ID: {}", i + 1, instanceId);
+
+ OrchestrationMetadata result = client.waitForInstanceCompletion(
+ instanceId, Duration.ofSeconds(120), true);
+
+ if (result != null) {
+ if (result.isCompleted()) {
+ completedOrchestrations++;
+ logger.info("Orchestration {} completed: {}", instanceId, result.readOutputAs(String.class));
+ } else {
+ failedOrTimedOutOrchestrations++;
+ logger.error("Orchestration {} did not complete successfully: {}", instanceId, result.getRuntimeStatus());
+ }
+ } else {
+ failedOrTimedOutOrchestrations++;
+ logger.warn("Orchestration {} did not complete within the timeout period", instanceId);
+ }
+ }
+
+ logger.info("=== Entity Demo Complete ===");
+ logger.info("Direct entity signals sent to '{}'", entityKey);
+ logger.info("Orchestrations completed successfully: {}/{}", completedOrchestrations, totalOrchestrations);
+ logger.info("Orchestrations failed or timed out: {}", failedOrTimedOutOrchestrations);
+
+ // Shutdown the worker and exit
+ worker.stop();
+ System.exit(0);
+ }
+}
diff --git a/samples/durable-task-sdks/java/entities/src/main/resources/logback-spring.xml b/samples/durable-task-sdks/java/entities/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000..934d9b4
--- /dev/null
+++ b/samples/durable-task-sdks/java/entities/src/main/resources/logback-spring.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+