Skip to content

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. The examples use the released 3.0.1 artifacts. Before upgrading from 2.x, see Upgrade from 2.x to 3.x.

<dependency>
    <groupId>software.amazon.lambda.durable</groupId>
    <artifactId>aws-durable-execution-sdk-java</artifactId>
    <version>3.0.1</version>
</dependency>
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-core</artifactId>
    <version>1.4.1</version>
</dependency>

For local and cloud tests, add the testing SDK in test scope:

<dependency>
    <groupId>software.amazon.lambda.durable</groupId>
    <artifactId>aws-durable-execution-sdk-java-testing</artifactId>
    <version>3.0.1</version>
    <scope>test</scope>
</dependency>

Package the Lambda as a shaded jar so the SDK and its dependencies are available at runtime:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.6.2</version>
    <configuration>
        <createDependencyReducedPom>false</createDependencyReducedPom>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals><goal>shade</goal></goals>
        </execution>
    </executions>
</plugin>

Usage

Extend DurableHandler<I, O> and implement handleRequest(I input, DurableContext ctx). Use the context to create steps, waits, callbacks, child contexts, invokes, maps, and parallel branches.

import java.time.Duration;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class OrderProcessor extends DurableHandler<Order, OrderResult> {

    @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.

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<Input, Output> {

    @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. 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. 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. Suppress replay logs; oldKeyNames=false
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 plugin factories explicitly. See Plugins. 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:

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:

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:

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 factories in application code. Each factory creates a fresh plugin for each Lambda invocation.

Java can also load plugin providers from the application class path. Set DURABLE_EXECUTION_PLUGINS to an ordered, comma-separated list of provider names:

DURABLE_EXECUTION_PLUGINS=otel,com.example.audit

The otel provider selects the default OpenTelemetry execution view and requires SDK/plugin 3.1.0 or later.

The SDK discovers DurableExecutionPluginProvider implementations with ServiceLoader and selects the providers named in the environment variable. Each provider implements createPlugin(InvocationInfo) to return a fresh plugin instance. Selected dynamically loaded plugins run first in the listed order, followed by plugins created by factories supplied through withPlugins(...).

For a Lambda layer, place the provider JAR under java/lib. The JAR must contain this service-provider configuration file:

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 selected providers without a concrete createPlugin(InvocationInfo) implementation cause DurableConfig construction to throw IllegalStateException.

Upgrade from 2.x to 3.x

Core 3.0 creates a fresh instrumentation plugin for each Lambda invocation. This isolates plugin state when invocations run concurrently. Applications that register no plugins, select none through DURABLE_EXECUTION_PLUGINS, and ship no plugin providers or plugin JARs can update their dependencies without changing handler or durable operation calls.

Upgrade dependencies together

The Java 3.0.1 release is available on Maven Central. Use these coordinated versions; all artifacts use group ID software.amazon.lambda.durable:

Artifact Version
aws-durable-execution-sdk-java 3.0.1
aws-durable-execution-sdk-java-plugin-otel 3.0.1
aws-durable-execution-sdk-java-testing 3.0.1
aws-durable-execution-sdk-java-plugin-telemetry-exporter 3.0.1

Update the artifacts your application uses together, including test dependencies and Lambda layers. OTel and the Telemetry Exporter remain optional. Java's durable OTel plugin follows the SDK version and uses 3.0.1, while TypeScript and Python version their OTel packages separately as 2.0.0. Check mvn dependency:tree for older transitive SDK or plugin artifacts.

Update plugin registrations

Replace withPlugins(pluginInstance) with a DurableExecutionPluginFactory, such as withPlugins(info -> new MyPlugin()). Keep shared exporters and connection pools outside the factory lambda, and keep invocation state on the new plugin.

Replace OTel plugin constructors with InvocationOtelPlugin.factory(...) or ExecutionOtelPlugin.factory(...). Choose one OTel view across code and environment configuration.

Rebuild plugin registration code and all provider JARs against 3.x. Providers now implement createPlugin(InvocationInfo); remove API_VERSION, getApiVersion(), and getPluginType(). Update provider layers as well as the function package. Replace DurableConfig.getPluginRunner() with getPluginFactories() when inspecting configured plugins.

Use config.toBuilder() when copying a configuration so the effective factory list is preserved without repeating environment discovery. Move custom exclusivity metadata from instance @ExclusivePluginGroup annotations to the factory's getExclusiveGroup().

If you use DurableExecutionTelemetry.durableExecutionTelemetry(config), upgrade the optional telemetry-exporter artifact to 3.0.1 too. Its return type changes to a factory, so recompile the registration code even though the source call stays the same.

The Telemetry Exporter is stable in 3.0.0. If you used a preview build, review the release notes for Lambda log envelope and timestamp changes before updating record consumers.

Validate the upgrade

Compile and test the application with the coordinated artifacts. Exercise suspension, resume, and concurrent invocations, then verify plugin output. A stale discovered provider fails startup, while a stale directly registered provider can log a warning and produce no telemetry. Hooks within one invocation can still run on multiple threads. Preserve operation names, order, nesting, and serialization while executions remain in flight.

For all API changes and before/after examples, see the Java 2.x to 3.x migration guide.

2.x Upgrade

When upgrading from 1.x to 2.x, review the Java SDK migration guide in the SDK repository. 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:

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:

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.

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.

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-serialization</artifactId>
    <version>VERSION</version>
</dependency>

Then provide a SerDes adapter:

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> T deserialize(String data, TypeToken<T> typeToken) {
        Type type = typeToken.getType();
        if (type instanceof Class<?> clazz && isLambdaEvent(clazz)) {
            @SuppressWarnings("unchecked")
            var serializer = LambdaEventSerializers.serializerFor((Class<T>) 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. 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(...):

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. The generated Javadoc is published at aws.github.io/aws-durable-execution-sdk-java/javadoc.