Durable Task Java SDK Setup and Deployment
January 30, 2026 ยท View on GitHub
Comprehensive setup guide for Azure Durable Task Scheduler with Java applications.
Local Development with Docker Emulator
Quick Start
# Pull the emulator image
docker pull mcr.microsoft.com/dts/dts-emulator:latest
# Run the emulator
docker run -d \
-p 8080:8080 \
-p 8082:8082 \
--name dts-emulator \
mcr.microsoft.com/dts/dts-emulator:latest
# Dashboard available at http://localhost:8082
Docker Compose Setup
# docker-compose.yml
version: '3.8'
services:
dts-emulator:
image: mcr.microsoft.com/dts/dts-emulator:latest
ports:
- "8080:8080" # gRPC endpoint
- "8082:8082" # Dashboard
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8082"]
interval: 5s
timeout: 3s
retries: 3
worker:
build: .
depends_on:
dts-emulator:
condition: service_healthy
environment:
- DURABLE_TASK_CONNECTION_STRING=Endpoint=http://dts-emulator:8080;TaskHub=default;Authentication=None
Multi-Hub Development
# docker-compose-multi-hub.yml
version: '3.8'
services:
dts-emulator:
image: mcr.microsoft.com/dts/dts-emulator:latest
ports:
- "8080:8080"
- "8082:8082"
order-worker:
build: ./order-service
environment:
- DURABLE_TASK_CONNECTION_STRING=Endpoint=http://dts-emulator:8080;TaskHub=orders;Authentication=None
notification-worker:
build: ./notification-service
environment:
- DURABLE_TASK_CONNECTION_STRING=Endpoint=http://dts-emulator:8080;TaskHub=notifications;Authentication=None
Azure Durable Task Scheduler Provisioning
Prerequisites
# Install/update Azure CLI
brew install azure-cli # macOS
# or
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash # Linux
# Login to Azure
az login
# Set subscription
az account set --subscription "your-subscription-id"
Create Durable Task Scheduler
# Variables
RESOURCE_GROUP="my-dts-rg"
LOCATION="eastus"
SCHEDULER_NAME="my-dts-scheduler"
TASKHUB_NAME="my-taskhub"
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
# Create Durable Task Scheduler namespace
az durabletask scheduler create \
--name $SCHEDULER_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku "standard"
# Create a Task Hub
az durabletask taskhub create \
--scheduler-name $SCHEDULER_NAME \
--resource-group $RESOURCE_GROUP \
--name $TASKHUB_NAME
# Get the endpoint
ENDPOINT=$(az durabletask scheduler show \
--name $SCHEDULER_NAME \
--resource-group $RESOURCE_GROUP \
--query "properties.endpoint" -o tsv)
echo "Connection String: Endpoint=$ENDPOINT;TaskHub=$TASKHUB_NAME;Authentication=DefaultAzure"
Bicep Template
// main.bicep
@description('Name of the Durable Task Scheduler')
param schedulerName string
@description('Location for resources')
param location string = resourceGroup().location
@description('Task Hub name')
param taskHubName string = 'default'
@description('SKU for the scheduler')
@allowed(['basic', 'standard', 'premium'])
param sku string = 'standard'
resource scheduler 'Microsoft.DurableTask/schedulers@2025-11-01' = {
name: schedulerName
location: location
properties: {
sku: {
name: sku
}
}
}
resource taskHub 'Microsoft.DurableTask/schedulers/taskHubs@2025-11-01' = {
parent: scheduler
name: taskHubName
properties: {}
}
output endpoint string = scheduler.properties.endpoint
output connectionString string = 'Endpoint=${scheduler.properties.endpoint};TaskHub=${taskHubName};Authentication=DefaultAzure'
Deploy with:
az deployment group create \
--resource-group $RESOURCE_GROUP \
--template-file main.bicep \
--parameters schedulerName=$SCHEDULER_NAME taskHubName=$TASKHUB_NAME
Authentication Configuration
DefaultAzureCredential (Recommended)
Works locally with Azure CLI credentials and in Azure with Managed Identity:
String connectionString = "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=DefaultAzure";
DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder()
.connectionString(connectionString)
.addOrchestration("MyOrchestration", ctx -> { /* ... */ })
.build();
Dependencies required:
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.11.0</version>
</dependency>
Managed Identity
// System-assigned managed identity
String connectionString = "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity";
// User-assigned managed identity
String connectionString = "Endpoint=https://my-scheduler.region.durabletask.io;TaskHub=my-hub;Authentication=ManagedIdentity;ClientId=<client-id>";
Azure CLI (Local Development)
# Login to Azure CLI
az login
# Your Java app will automatically use Azure CLI credentials
# with Authentication=DefaultAzure
Role Assignments
Grant the worker/client identity the Durable Task Data Owner role:
# Get the scheduler resource ID
SCHEDULER_ID=$(az durabletask scheduler show \
--name $SCHEDULER_NAME \
--resource-group $RESOURCE_GROUP \
--query id -o tsv)
# Assign role to managed identity
az role assignment create \
--assignee "<principal-id>" \
--role "Durable Task Data Owner" \
--scope $SCHEDULER_ID
Application Integration
Console Application
// Main.java
import com.microsoft.durabletask.*;
import com.microsoft.durabletask.azuremanaged.*;
import java.time.Duration;
public class Main {
public static void main(String[] args) throws Exception {
String connectionString = getConnectionString();
// Create worker
DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder()
.connectionString(connectionString)
.addOrchestration("ProcessOrder", Orchestrations::processOrder)
.addActivity("ValidateOrder", Activities::validateOrder)
.addActivity("ProcessPayment", Activities::processPayment)
.addActivity("SendConfirmation", Activities::sendConfirmation)
.build();
// Start worker in background thread
Thread workerThread = new Thread(() -> {
try {
worker.start();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
workerThread.start();
// Create client
DurableTaskClient client = new DurableTaskGrpcClientBuilder()
.connectionString(connectionString)
.build();
// Schedule orchestration
OrderInput input = new OrderInput("order-123", 99.99);
String instanceId = client.scheduleNewOrchestrationInstance("ProcessOrder", input);
System.out.println("Started: " + instanceId);
// Wait for result
OrchestrationMetadata result = client.waitForInstanceCompletion(
instanceId, Duration.ofMinutes(5), true);
System.out.println("Status: " + result.getRuntimeStatus());
System.out.println("Output: " + result.readOutputAs(String.class));
// Cleanup
worker.close();
client.close();
}
private static String getConnectionString() {
String cs = System.getenv("DURABLE_TASK_CONNECTION_STRING");
return cs != null ? cs : "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";
}
}
Spring Boot Integration
// DurableTaskConfig.java
import com.microsoft.durabletask.*;
import com.microsoft.durabletask.azuremanaged.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.*;
@Configuration
public class DurableTaskConfig {
@Value("${durable-task.connection-string}")
private String connectionString;
@Bean
public DurableTaskClient durableTaskClient() {
return new DurableTaskGrpcClientBuilder()
.connectionString(connectionString)
.build();
}
@Bean
public DurableTaskGrpcWorker durableTaskWorker(
List<OrchestrationDefinition> orchestrations,
List<ActivityDefinition> activities) {
DurableTaskGrpcWorkerBuilder builder = new DurableTaskGrpcWorkerBuilder()
.connectionString(connectionString);
for (OrchestrationDefinition orch : orchestrations) {
builder.addOrchestration(orch.getName(), orch.getImplementation());
}
for (ActivityDefinition act : activities) {
builder.addActivity(act.getName(), act.getImplementation());
}
return builder.build();
}
}
// WorkerLifecycle.java
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
@Component
public class WorkerLifecycle {
private final DurableTaskGrpcWorker worker;
private Thread workerThread;
public WorkerLifecycle(DurableTaskGrpcWorker worker) {
this.worker = worker;
}
@PostConstruct
public void start() {
workerThread = new Thread(() -> {
try {
worker.start();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
workerThread.setDaemon(true);
workerThread.start();
}
@PreDestroy
public void stop() {
try {
worker.close();
} catch (Exception e) {
// Log error
}
}
}
// WorkflowController.java
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/workflows")
public class WorkflowController {
private final DurableTaskClient client;
public WorkflowController(DurableTaskClient client) {
this.client = client;
}
@PostMapping("/orders")
public WorkflowResponse startOrder(@RequestBody OrderInput input) {
String instanceId = client.scheduleNewOrchestrationInstance("ProcessOrder", input);
return new WorkflowResponse(instanceId, "Started");
}
@GetMapping("/{instanceId}")
public WorkflowStatus getStatus(@PathVariable String instanceId) throws Exception {
OrchestrationMetadata metadata = client.getInstanceMetadata(instanceId, true);
return new WorkflowStatus(
instanceId,
metadata.getRuntimeStatus().toString(),
metadata.getCustomStatus()
);
}
@PostMapping("/{instanceId}/events/{eventName}")
public void raiseEvent(
@PathVariable String instanceId,
@PathVariable String eventName,
@RequestBody Object eventData) {
client.raiseEvent(instanceId, eventName, eventData);
}
}
application.yml for Spring Boot
durable-task:
connection-string: ${DURABLE_TASK_CONNECTION_STRING:Endpoint=http://localhost:8080;TaskHub=default;Authentication=None}
Deployment Options
Container Apps
# container-app.yaml
apiVersion: apps/v1
kind: ContainerApp
metadata:
name: dts-worker
spec:
template:
containers:
- name: worker
image: myregistry.azurecr.io/my-worker:latest
env:
- name: DURABLE_TASK_CONNECTION_STRING
secretRef: dts-connection-string
resources:
cpu: 0.5
memory: 1Gi
scale:
minReplicas: 1
maxReplicas: 10
rules:
- name: queue-scaling
custom:
type: external
metadata:
scalerAddress: "azure-scheduler-scaler:5050"
Kubernetes Deployment
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: dts-worker
spec:
replicas: 3
selector:
matchLabels:
app: dts-worker
template:
metadata:
labels:
app: dts-worker
spec:
serviceAccountName: dts-worker-sa
containers:
- name: worker
image: myregistry.azurecr.io/my-worker:latest
env:
- name: DURABLE_TASK_CONNECTION_STRING
valueFrom:
secretKeyRef:
name: dts-secrets
key: connection-string
- name: AZURE_CLIENT_ID # For workload identity
valueFrom:
secretKeyRef:
name: dts-secrets
key: client-id
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
Dockerfile
# Dockerfile
FROM eclipse-temurin:17-jdk as builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN apt-get update && apt-get install -y maven
RUN mvn clean package -DskipTests
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Logging and Monitoring
SLF4J Configuration
<!-- logback.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.microsoft.durabletask" level="INFO"/>
<logger name="io.grpc" level="WARN"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
Application Insights Integration
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>applicationinsights-runtime-attach</artifactId>
<version>3.4.18</version>
</dependency>
// Enable in main method
import com.microsoft.applicationinsights.attach.ApplicationInsights;
public class Main {
public static void main(String[] args) {
ApplicationInsights.attach();
// ... rest of application
}
}
Health Check Endpoint
// For containerized deployments, add health endpoints
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
public class HealthServer {
private final HttpServer server;
public HealthServer(int port) throws IOException {
server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext("/health", exchange -> {
String response = "{\"status\":\"healthy\"}";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
});
server.createContext("/ready", exchange -> {
String response = "{\"status\":\"ready\"}";
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
});
server.setExecutor(null);
}
public void start() {
server.start();
}
public void stop() {
server.stop(0);
}
}
Maven Project Template
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>durable-task-worker</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<durabletask.version>1.6.2</durabletask.version>
</properties>
<dependencies>
<!-- Durable Task SDK -->
<dependency>
<groupId>com.microsoft.durabletask</groupId>
<artifactId>durabletask-client</artifactId>
<version>${durabletask.version}</version>
</dependency>
<dependency>
<groupId>com.microsoft.durabletask</groupId>
<artifactId>durabletask-azuremanaged</artifactId>
<version>${durabletask.version}</version>
</dependency>
<!-- Azure Identity -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.11.0</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.example.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Testing
Unit Testing Orchestrations
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class OrchestrationTests {
@Test
void testOrderWorkflowSuccess() {
// Create mock context
TaskOrchestrationContext ctx = mock(TaskOrchestrationContext.class);
// Setup input
OrderInput input = new OrderInput("order-123", 99.99);
when(ctx.getInput(OrderInput.class)).thenReturn(input);
// Setup activity calls
when(ctx.callActivity(eq("ValidateOrder"), any(), eq(Boolean.class)))
.thenReturn(completedTask(true));
when(ctx.callActivity(eq("ProcessPayment"), any(), eq(PaymentResult.class)))
.thenReturn(completedTask(new PaymentResult(true, "tx-123")));
when(ctx.callActivity(eq("SendConfirmation"), any(), eq(Void.class)))
.thenReturn(completedTask(null));
// Execute orchestration
Object result = Orchestrations.processOrder(ctx);
// Verify
assertNotNull(result);
verify(ctx).callActivity(eq("ValidateOrder"), any(), eq(Boolean.class));
verify(ctx).callActivity(eq("ProcessPayment"), any(), eq(PaymentResult.class));
verify(ctx).callActivity(eq("SendConfirmation"), any(), eq(Void.class));
}
private <T> Task<T> completedTask(T value) {
Task<T> task = mock(Task.class);
when(task.await()).thenReturn(value);
return task;
}
}
Integration Testing with Emulator
import org.junit.jupiter.api.*;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class IntegrationTests {
@Container
static GenericContainer<?> emulator = new GenericContainer<>("mcr.microsoft.com/dts/dts-emulator:latest")
.withExposedPorts(8080, 8082);
private DurableTaskClient client;
private DurableTaskGrpcWorker worker;
@BeforeEach
void setup() {
String connectionString = String.format(
"Endpoint=http://%s:%d;TaskHub=test;Authentication=None",
emulator.getHost(),
emulator.getMappedPort(8080)
);
worker = new DurableTaskGrpcWorkerBuilder()
.connectionString(connectionString)
.addOrchestration("TestOrchestration", ctx -> {
String input = ctx.getInput(String.class);
return "Hello, " + input + "!";
})
.build();
new Thread(() -> {
try { worker.start(); } catch (Exception e) {}
}).start();
client = new DurableTaskGrpcClientBuilder()
.connectionString(connectionString)
.build();
}
@AfterEach
void teardown() throws Exception {
worker.close();
client.close();
}
@Test
void testSimpleOrchestration() throws Exception {
String instanceId = client.scheduleNewOrchestrationInstance("TestOrchestration", "World");
OrchestrationMetadata result = client.waitForInstanceCompletion(
instanceId, Duration.ofSeconds(30), true);
assertEquals(OrchestrationRuntimeStatus.COMPLETED, result.getRuntimeStatus());
assertEquals("Hello, World!", result.readOutputAs(String.class));
}
}