# Java SDK The Java SDK (`aws-durable-execution-sdk-java`) runs in your Lambda functions and provides `DurableHandler`, `DurableContext`, durable operations, configurable serialization, and local/cloud testing utilities. The SDK supports Java 17 and newer. Virtual threads require Java 21. ## Installation Add the execution SDK to the Lambda function package. Replace the version with the latest release from [Maven Central](https://central.sonatype.com/artifact/software.amazon.lambda.durable/aws-durable-execution-sdk-java). ```xml software.amazon.lambda.durable aws-durable-execution-sdk-java 2.2.0 com.amazonaws aws-lambda-java-core 1.4.0 ``` For local and cloud tests, add the testing SDK in test scope: ```xml software.amazon.lambda.durable aws-durable-execution-sdk-java-testing 2.2.0 test ``` Package the Lambda as a shaded jar so the SDK and its dependencies are available at runtime: ```xml org.apache.maven.plugins maven-shade-plugin 3.6.2 false package shade ``` ## Usage Extend `DurableHandler` and implement `handleRequest(I input, DurableContext ctx)`. Use the context to create steps, waits, callbacks, child contexts, invokes, maps, and parallel branches. ```java import java.time.Duration; import software.amazon.lambda.durable.DurableContext; import software.amazon.lambda.durable.DurableHandler; public class OrderProcessor extends DurableHandler { @Override public OrderResult handleRequest(Order order, DurableContext ctx) { var reservation = ctx.step("reserve-inventory", Reservation.class, stepCtx -> inventoryService.reserve(order.items())); ctx.wait("warehouse-delay", Duration.ofHours(2)); var shipment = ctx.step("ship-order", Shipment.class, stepCtx -> shippingService.ship(reservation, order.address())); return new OrderResult(order.id(), shipment.trackingNumber()); } } ``` Operation names are required in Java. Use stable, descriptive names so replay can match new execution to checkpointed state. ## Configuration Override `createConfiguration()` to customize serialization, the Lambda client, logging, polling, checkpoint batching, plugins, or the executor used for user-defined async work. ```java import java.time.Duration; import java.util.concurrent.Executors; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.lambda.LambdaClient; import software.amazon.lambda.durable.DurableConfig; import software.amazon.lambda.durable.DurableContext; import software.amazon.lambda.durable.DurableHandler; public class ConfiguredHandler extends DurableHandler { @Override protected DurableConfig createConfiguration() { var lambdaClientBuilder = LambdaClient.builder() .region(Region.US_WEST_2); return DurableConfig.builder() .withLambdaClientBuilder(lambdaClientBuilder) .withExecutorService(Executors.newFixedThreadPool(16)) .withCheckpointDelay(Duration.ofMillis(10)) .build(); } @Override public Output handleRequest(Input input, DurableContext ctx) { // Durable function logic } } ``` The configured executor is used for user operations such as async steps and concurrent branches. Internal SDK polling and checkpoint coordination use SDK-managed threads. ### DurableConfig options `DurableConfig.builder()` supports the following Java SDK settings: | Builder method | Purpose | Default | | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `withLambdaClientBuilder(...)` | Customize the production AWS SDK client. See [Custom Lambda client](../../configuration/custom-lambda-client.md). | Client configured from the Lambda environment | | `withDurableExecutionClient(...)` | Supply a `DurableExecutionClient`, primarily for tests and custom infrastructure. | Lambda-backed client | | `withSerDes(...)` | Set the default serializer for handler inputs, outputs, and durable operation state. See [Serialization](../../state/serialization.md). | `JacksonSerDes` | | `withExecutorService(...)` | Set the executor for user-defined async and concurrent work. | Cached daemon thread pool | | `withLoggerConfig(...)` | Configure replay suppression and MDC key compatibility. See [Logging](../../observability/logging.md). | Suppress replay logs and use 2.x MDC keys | | `withPollingStrategy(...)` | Configure delays while polling the durable execution backend. | `PollingStrategies.Presets.DEFAULT` | | `withCheckpointDelay(...)` | Delay checkpoint submission briefly so concurrent updates can be batched. | `Duration.ZERO` | | `withDeserializeAfterSerialization(...)` | Round-trip serialized operation results and exceptions before checkpointing. | `true` | | `withCheckpointEmptyMap(...)` | Emit checkpoints for empty map operations. This is a temporary compatibility option. | `false` | | `withPlugins(...)` | Register lifecycle plugins explicitly. See [Plugins](../../observability/plugins.md). | No explicit plugins | `withDurableExecutionClient(...)` replaces the SDK's backend client. For normal production customization such as credentials, Region, HTTP settings, or AWS SDK retry policies, use `withLambdaClientBuilder(...)`. ### Backend polling Backend polling is distinct from durable waits and user-code retry strategies. It controls how often the SDK checks for checkpoint and operation status updates. The default uses exponential backoff with a 1-second base interval, a 2x backoff rate, full jitter, and a 10-second maximum interval. Configure a different strategy with `PollingStrategies`: ```java var polling = PollingStrategies.exponentialBackoff( Duration.ofMillis(500), 1.5, JitterStrategy.HALF, Duration.ofSeconds(5)); return DurableConfig.builder() .withPollingStrategy(polling) .build(); ``` Use `PollingStrategies.fixedDelay(Duration)` for a constant interval or `PollingStrategies.at(Instant)` to schedule a poll at a specific time. ### Checkpoint batching By default, `withCheckpointDelay(Duration.ZERO)` submits checkpoint updates as soon as possible. A short delay lets the SDK combine updates produced by concurrent operations into fewer backend requests: ```java return DurableConfig.builder() .withCheckpointDelay(Duration.ofMillis(10)) .build(); ``` A longer delay can improve batching but also increases the time before completed work becomes durable. Use a small value and validate the latency tradeoff for your workload. ### Serialization round-trip validation Java immediately deserializes serialized operation results and exceptions by default. This catches incompatible custom `SerDes` implementations before checkpointing and keeps values returned during initial execution consistent with values returned during replay. You can disable the additional deserialize pass: ```java return DurableConfig.builder() .withDeserializeAfterSerialization(false) .build(); ``` !!! warning Disabling validation can hide serialization failures until replay, and initial execution may return a different value shape than replay. Custom serializers must remain round-trip safe even when validation is disabled. ### Load plugins dynamically `withPlugins(...)` registers plugin instances in application code. Java can also load plugin providers from the application class path. Set `DURABLE_EXECUTION_PLUGINS` to an ordered, comma-separated list of provider names: ```text DURABLE_EXECUTION_PLUGINS=otel-invocation,com.example.audit ``` The SDK discovers `DurableExecutionPluginProvider` implementations with `ServiceLoader`, but creates only the providers named in the environment variable. Dynamically loaded plugins run first in the listed order, followed by plugins supplied through `withPlugins(...)`. For a Lambda layer, place the provider JAR under `java/lib`. The JAR must contain this service-provider configuration file: ```text META-INF/services/software.amazon.lambda.durable.plugin.DurableExecutionPluginProvider ``` The file contains the provider implementation class name. If you shade multiple providers into one JAR, configure the build to merge `META-INF/services` entries. Invalid or missing providers, duplicate configured or discovered provider names, and incompatible provider API versions cause `DurableConfig` construction to throw `IllegalStateException`. ## 2.x Upgrade When upgrading from `1.x` to `2.x`, review the Java SDK migration guide in the [SDK repository](https://github.com/aws/aws-durable-execution-sdk-java/blob/main/docs/migration-1.x-to-2.x.md). The main changes are: - Replace `StepConfig.builder().semantics(...)` with `semanticsPerRetry(...)`. - Preserve old `AT_MOST_ONCE_PER_RETRY` behavior by also setting `retryStrategy(RetryStrategies.Presets.NO_RETRY)`. - Update log queries and dashboards from `durableExecutionArn`, `contextId`, and `contextName` to `executionArn`, `operationId`, and `operationName`. - Move replay checks to `DurableContext.isReplaying()`; `StepContext` no longer exposes replay state. - Update tests and error handling that expect invalid context usage to throw `IllegalDurableOperationException`; `2.x` throws `IllegalStateException`. - Verify custom `SerDes` implementations can deserialize values immediately after serialization. `2.x` validates this round trip before checkpointing by default. Useful searches before upgrading: ```console rg -n "\.semantics\(" . rg -n "durableExecutionArn|contextId|contextName" . rg -n "isReplaying|StepContext" . ``` If you need a temporary log compatibility window, configure old MDC key names while dashboards are migrated: ```java import software.amazon.lambda.durable.DurableConfig; import software.amazon.lambda.durable.logging.LoggerConfig; @Override protected DurableConfig createConfiguration() { return DurableConfig.builder() .withLoggerConfig(new LoggerConfig(true, true)) .build(); } ``` ## FAQ ### Can I use a virtual thread pool? Yes. The SDK baseline is Java 17, so it does not use virtual threads by default. On Java 21 or newer, provide a virtual-thread executor through `DurableConfig`. ```java import java.util.concurrent.Executors; import software.amazon.lambda.durable.DurableConfig; @Override protected DurableConfig createConfiguration() { return DurableConfig.builder() .withExecutorService(Executors.newVirtualThreadPerTaskExecutor()) .build(); } ``` This executor only runs user-defined operation code. Virtual threads can help when you create many concurrent async steps or branches, but they do not remove Lambda memory, timeout, downstream service, or durable execution service limits. ### Can a Java durable function be triggered by SQS, SNS, EventBridge, or other event sources? Yes, but Java event model deserialization needs care. The default user-data serializer is the SDK's Jackson-based `JacksonSerDes`, not the same serializer the Lambda Java runtime uses for event classes from `aws-lambda-java-events`. For broad event-source support, add the Lambda Java serialization library, which provides the `com.amazonaws.services.lambda.runtime.serialization` package, and route Lambda event classes through it. ```xml com.amazonaws aws-lambda-java-serialization VERSION ``` Then provide a `SerDes` adapter: ```java import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; import java.lang.reflect.Type; import software.amazon.lambda.durable.DurableConfig; import software.amazon.lambda.durable.TypeToken; import software.amazon.lambda.durable.serde.JacksonSerDes; import software.amazon.lambda.durable.serde.SerDes; @Override protected DurableConfig createConfiguration() { return DurableConfig.builder() .withSerDes(new LambdaEventSerDes()) .build(); } final class LambdaEventSerDes implements SerDes { private final SerDes fallback = new JacksonSerDes(); private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); @Override public String serialize(Object value) { return fallback.serialize(value); } @Override public T deserialize(String data, TypeToken typeToken) { Type type = typeToken.getType(); if (type instanceof Class clazz && isLambdaEvent(clazz)) { @SuppressWarnings("unchecked") var serializer = LambdaEventSerializers.serializerFor((Class) clazz, classLoader); return serializer.fromJson(data); } return fallback.deserialize(data, typeToken); } private static boolean isLambdaEvent(Class clazz) { return clazz.getName().startsWith("com.amazonaws.services.lambda.runtime.events."); } } ``` This pattern addresses the event-trigger deserialization problem discussed in [aws/aws-durable-execution-sdk-java#366](https://github.com/aws/aws-durable-execution-sdk-java/issues/366). The configured `SerDes` is also used as the default for steps, child contexts, callbacks, and other operations unless an operation-specific config provides its own serializer. ### Can I use Lambda SnapStart? Yes, but avoid the Java SDK default Lambda client if SnapStart restores a snapshot after the environment-variable credentials captured at initialization have expired. The default client uses `EnvironmentVariableCredentialsProvider`, which can cause authentication failures after restore. For SnapStart, provide your own `LambdaClient` with a credentials provider that refreshes after restore, then pass it to `withLambdaClientBuilder(...)`: ```java import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.lambda.LambdaClient; import software.amazon.lambda.durable.DurableConfig; @Override protected DurableConfig createConfiguration() { var lambdaClientBuilder = LambdaClient.builder() .region(Region.of(System.getenv("AWS_REGION"))) .credentialsProvider(ContainerCredentialsProvider.builder().build()); return DurableConfig.builder() .withLambdaClientBuilder(lambdaClientBuilder) .build(); } ``` If you customize the Lambda client for SnapStart, keep the region explicit and avoid caching resolved credentials yourself during initialization. ### Where is the Java SDK source and API reference? The source is in [aws/aws-durable-execution-sdk-java](https://github.com/aws/aws-durable-execution-sdk-java). The generated Javadoc is published at [aws.github.io/aws-durable-execution-sdk-java/javadoc](https://aws.github.io/aws-durable-execution-sdk-java/javadoc/).