| name | durable-task-java |
| description | Build durable, fault-tolerant workflows in Java using the Durable Task SDK with Azure Durable Task Scheduler. Use when creating orchestrations, activities, or implementing patterns like function chaining, fan-out/fan-in, human interaction, or monitoring. Applies to any Java application requiring durable execution, state persistence, or distributed transactions without Azure Functions dependency. |
Durable Task Java SDK with Durable Task Scheduler
Build fault-tolerant, stateful workflows in Java applications using the Durable Task SDK connected to Azure Durable Task Scheduler.
Quick Start
Maven Dependencies
<dependencies>
<dependency>
<groupId>com.microsoft</groupId>
<artifactId>durabletask-client</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>com.microsoft</groupId>
<artifactId>durabletask-azuremanaged</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.32</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.78.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.78.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.78.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Gradle Dependencies
def grpcVersion = '1.78.0'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation 'com.microsoft:durabletask-client:1.7.0'
implementation 'com.microsoft:durabletask-azuremanaged:1.7.0'
implementation 'com.azure:azure-identity:1.18.2'
// Logging
implementation 'ch.qos.logback:logback-classic:1.5.32'
implementation 'org.slf4j:slf4j-api:2.0.17'
// gRPC
implementation "io.grpc:grpc-protobuf:${grpcVersion}"
implementation "io.grpc:grpc-stub:${grpcVersion}"
runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"
}
Minimal Worker + Client Setup
import com.microsoft.durabletask.*;
import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerClientExtensions;
import com.microsoft.durabletask.azuremanaged.DurableTaskSchedulerWorkerExtensions;
import java.time.Duration;
public class DurableTaskApp {
public static void main(String[] args) throws Exception {
String connectionString = System.getenv("DURABLE_TASK_CONNECTION_STRING");
if (connectionString == null) {
connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
}
DurableTaskGrpcWorker worker = DurableTaskSchedulerWorkerExtensions.createWorkerBuilder(connectionString)
.addOrchestration(new TaskOrchestrationFactory() {
@Override
public String getName() {
return "MyOrchestration";
}
@Override
public TaskOrchestration create() {
return ctx -> {
String input = ctx.getInput(String.class);
String result = ctx.callActivity("SayHello", input, String.class).await();
ctx.complete(result);
};
}
})
.addActivity(new TaskActivityFactory() {
@Override
public String getName() {
return "SayHello";
}
@Override
public TaskActivity create() {
return ctx -> {
String name = ctx.getInput(String.class);
return "Hello " + name + "!";
};
}
})
.build();
worker.start();
DurableTaskClient client = DurableTaskSchedulerClientExtensions.createClientBuilder(connectionString).build();
String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration", "World");
System.out.println("Started orchestration: " + instanceId);
OrchestrationMetadata result = client.waitForInstanceCompletion(
instanceId, Duration.ofSeconds(60), true);
System.out.println("Result: " + result.readOutputAs(String.class));
worker.close();
}
}
Pattern Selection Guide
| Pattern | Use When |
|---|
| Function Chaining | Sequential steps where each depends on the previous |
| Fan-Out/Fan-In | Parallel processing with aggregated results |
| Human Interaction | Workflow pauses for external input/approval |
| Sub-Orchestrations | Reusable workflow components or version isolation |
| Eternal Orchestrations | Long-running background processes with continueAsNew |
| Monitoring | Periodic polling with configurable timeouts |
See references/patterns.md for detailed implementations.
Orchestration Structure
Basic Orchestrator
.addOrchestration(new TaskOrchestrationFactory() {
@Override
public String getName() {
return "OrderWorkflow";
}
@Override
public TaskOrchestration create() {
return ctx -> {
OrderInfo order = ctx.getInput(OrderInfo.class);
boolean valid = ctx.callActivity("ValidateOrder", order, Boolean.class).await();
if (!valid) {
ctx.complete("Order invalid");
return;
}
String result = ctx.callActivity("ProcessOrder", order, String.class).await();
ctx.complete(result);
};
}
})
Basic Activity
.addActivity(new TaskActivityFactory() {
@Override
public String getName() {
return "ProcessOrder";
}
@Override
public TaskActivity create() {
return ctx -> {
OrderInfo order = ctx.getInput(OrderInfo.class);
System.out.println("Processing order: " + order.getOrderId());
return "Order " + order.getOrderId() + " processed";
};
}
})
Critical Rules
Orchestration Determinism
Orchestrations replay from history - all code MUST be deterministic. When an orchestration resumes, it replays all previous code to rebuild state. Non-deterministic code produces different results on replay, causing failures.
NEVER do inside orchestrations:
Instant.now(), LocalDateTime.now(), new Date() → Use ctx.getCurrentInstant()
UUID.randomUUID() → Use ctx.newUUID()
new Random() → Pass random values from activities
- Direct I/O, HTTP calls, database access → Move to activities
Thread.sleep() → Use ctx.createTimer()
System.getenv() that may change → Pass as input or use activities
- HashMap/HashSet iteration (non-deterministic order) → Use TreeMap/TreeSet
ALWAYS use:
ctx.callActivity("Name", input, Type.class).await() - Call activities
ctx.callSubOrchestrator("Name", input, Type.class).await() - Sub-orchestrations
ctx.createTimer(Duration).await() - Durable delays
ctx.waitForExternalEvent("EventName", timeout, Type.class).await() - External events
ctx.getCurrentInstant() - Current time (deterministic)
ctx.newUUID() - Generate UUIDs (deterministic)
ctx.setCustomStatus(status) - Set status
Non-Determinism Patterns (WRONG vs CORRECT)
Getting Current Time
.addOrchestration("BadOrchestration", ctx -> {
Instant currentTime = Instant.now();
if (currentTime.isBefore(deadline)) {
ctx.callActivity("ProcessNow", null, Void.class).await();
}
return null;
})
.addOrchestration("GoodOrchestration", ctx -> {
Instant currentTime = ctx.getCurrentInstant();
if (currentTime.isBefore(deadline)) {
ctx.callActivity("ProcessNow", null, Void.class).await();
}
return null;
})
Generating UUIDs
.addOrchestration("BadOrchestration", ctx -> {
String orderId = UUID.randomUUID().toString();
ctx.callActivity("CreateOrder", orderId, Void.class).await();
return orderId;
})
.addOrchestration("GoodOrchestration", ctx -> {
String orderId = ctx.newUUID().toString();
ctx.callActivity("CreateOrder", orderId, Void.class).await();
return orderId;
})
Random Numbers
.addOrchestration("BadOrchestration", ctx -> {
int delay = new Random().nextInt(10);
ctx.createTimer(Duration.ofSeconds(delay)).await();
return null;
})
.addActivity("GetRandomDelay", ctx -> {
return new Random().nextInt(10);
})
.addOrchestration("GoodOrchestration", ctx -> {
int delay = ctx.callActivity("GetRandomDelay", null, Integer.class).await();
ctx.createTimer(Duration.ofSeconds(delay)).await();
return null;
})
Sleeping/Delays
.addOrchestration("BadOrchestration", ctx -> {
ctx.callActivity("Step1", null, Void.class).await();
Thread.sleep(60000);
ctx.callActivity("Step2", null, Void.class).await();
return null;
})
.addOrchestration("GoodOrchestration", ctx -> {
ctx.callActivity("Step1", null, Void.class).await();
ctx.createTimer(Duration.ofMinutes(1)).await();
ctx.callActivity("Step2", null, Void.class).await();
return null;
})
HTTP Calls and I/O
.addOrchestration("BadOrchestration", ctx -> {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
})
.addActivity("FetchData", ctx -> {
String url = ctx.getInput(String.class);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.body();
})
.addOrchestration("GoodOrchestration", ctx -> {
String data = ctx.callActivity("FetchData",
"https://api.example.com/data", String.class).await();
return data;
})
Database Access
.addOrchestration("BadOrchestration", ctx -> {
Connection conn = DriverManager.getConnection(dbUrl);
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id=?");
return null;
})
.addActivity("GetUser", ctx -> {
String userId = ctx.getInput(String.class);
Connection conn = DriverManager.getConnection(dbUrl);
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id=?");
stmt.setString(1, userId);
ResultSet rs = stmt.executeQuery();
return user;
})
.addOrchestration("GoodOrchestration", ctx -> {
String userId = ctx.getInput(String.class);
User user = ctx.callActivity("GetUser", userId, User.class).await();
return user;
})
Environment Variables
.addOrchestration("BadOrchestration", ctx -> {
String apiEndpoint = System.getenv("API_ENDPOINT");
ctx.callActivity("CallApi", apiEndpoint, Void.class).await();
return null;
})
.addOrchestration("GoodOrchestration", ctx -> {
Config config = ctx.getInput(Config.class);
String apiEndpoint = config.getApiEndpoint();
ctx.callActivity("CallApi", apiEndpoint, Void.class).await();
return null;
})
.addActivity("CallApi", ctx -> {
String apiEndpoint = System.getenv("API_ENDPOINT");
return null;
})
Collection Iteration Order
.addOrchestration("BadOrchestration", ctx -> {
Map<String, Object> items = ctx.getInput(HashMap.class);
for (String key : items.keySet()) {
ctx.callActivity("Process", key, Void.class).await();
}
return null;
})
.addOrchestration("GoodOrchestration", ctx -> {
Map<String, Object> items = ctx.getInput(HashMap.class);
List<String> sortedKeys = new ArrayList<>(items.keySet());
Collections.sort(sortedKeys);
for (String key : sortedKeys) {
ctx.callActivity("Process", key, Void.class).await();
}
return null;
})
Using await()
In Java, orchestrator functions use .await() to wait for durable operations:
String result = ctx.callActivity("MyActivity", input, String.class).await();
Task<String> task = ctx.callActivity("MyActivity", input, String.class);
Error Handling
.addOrchestration("OrchestrationWithErrorHandling", ctx -> {
String input = ctx.getInput(String.class);
try {
String result = ctx.callActivity("RiskyActivity", input, String.class).await();
return result;
} catch (TaskFailedException ex) {
ctx.setCustomStatus(Map.of("error", ex.getMessage()));
ctx.callActivity("CompensationActivity", input, Void.class).await();
return "Compensated";
}
})
Retry Policies
TaskOptions options = new TaskOptions(new RetryPolicy(
3,
Duration.ofSeconds(5),
2.0,
Duration.ofMinutes(1),
Duration.ofMinutes(5)
));
ctx.callActivity("UnreliableActivity", input, String.class, options).await();
Connection & Authentication
Connection String Formats
"Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=DefaultAzure"
"Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity"
Connection Helper
public static String getConnectionString() {
String endpoint = System.getenv("ENDPOINT");
String taskHub = System.getenv("TASKHUB");
if (endpoint == null) endpoint = "http://localhost:8080";
if (taskHub == null) taskHub = "default";
String authType = endpoint.startsWith("http://localhost") ? "None" : "DefaultAzure";
return String.format("Endpoint=%s;TaskHub=%s;Authentication=%s",
endpoint, taskHub, authType);
}
Local Development with Emulator
docker pull mcr.microsoft.com/dts/dts-emulator:latest
docker run -d -p 8080:8080 -p 8082:8082 --name dts-emulator mcr.microsoft.com/dts/dts-emulator:latest
Client Operations
DurableTaskClient client = DurableTaskSchedulerClientExtensions.createClientBuilder(connectionString).build();
String instanceId = client.scheduleNewOrchestrationInstance("MyOrchestration", input);
String instanceId = client.scheduleNewOrchestrationInstance(
"MyOrchestration", input, "my-custom-id");
OrchestrationMetadata result = client.waitForInstanceCompletion(
instanceId, Duration.ofSeconds(60), true);
OrchestrationMetadata state = client.getInstanceMetadata(instanceId, true);
client.raiseEvent(instanceId, "ApprovalEvent", approvalData);
client.terminate(instanceId, "User cancelled");
client.suspendInstance(instanceId, "Pausing for maintenance");
client.resumeInstance(instanceId, "Resuming operation");
Troubleshooting
NullPointerException: Cannot invoke "TaskOrchestrationFactory.create()" because "factory" is null
This error means multiple workers with different orchestration registrations are connected to the same Task Hub simultaneously. When the scheduler dispatches an orchestration event, it may route it to a worker that does not have that orchestration type registered, causing a null factory lookup.
Root cause: Different sample applications or worker processes running at the same time against the same emulator endpoint and Task Hub. Worker A picks up an orchestration that was scheduled by Worker B, but Worker A doesn't have that orchestration registered.
Fix:
- Stop all running worker processes before starting a new sample
- Ensure only one worker type is connected to a given Task Hub at a time
- If using the local emulator, restart it to clear any queued orchestrations:
docker restart dts-emulator
- Alternatively, use different Task Hub names for each sample to isolate them
pkill -f "gradlew" || true
docker restart dts-emulator
References
- patterns.md - Detailed pattern implementations (Fan-Out/Fan-In, Human Interaction, Sub-Orchestrations)
- setup.md - Azure Durable Task Scheduler provisioning and deployment