DynamoDB UpdateItem in Java (AWS SDK v2)

The update itself is one builder call. What costs Java developers time is everything around it: a response object that never returns null, an exception hierarchy where the interesting failure is a subclass of the one you probably caught, and a high-level client that cannot express this operation at all.

Code

import java.util.HashMap;
import java.util.Map;

import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse;

public class UpdateItemExample {
    public static void main(String[] args) {
        try (DynamoDbClient ddb = DynamoDbClient.builder()
                .region(Region.US_EAST_1)
                .build()) {

            Map<String, AttributeValue> key = new HashMap<>();
            key.put("Artist", AttributeValue.builder().s("Arturo Sandoval").build());
            key.put("SongTitle", AttributeValue.builder().s("Cubano Chant").build());

            Map<String, String> names = new HashMap<>();
            names.put("#upd0", "Genre");
            names.put("#upd1", "Year");
            names.put("#upd2", "Awards");

            Map<String, AttributeValue> values = new HashMap<>();
            values.put(":updValue0", AttributeValue.builder().s("Latin Jazz").build());
            values.put(":updValue1", AttributeValue.builder().n("1994").build());
            values.put(":updValue2", AttributeValue.builder().n("1").build());

            UpdateItemRequest request = UpdateItemRequest.builder()
                    .tableName("Music")
                    .key(key)
                    .updateExpression("SET #upd0 = :updValue0, #upd1 = :updValue1 ADD #upd2 :updValue2")
                    .expressionAttributeNames(names)
                    .expressionAttributeValues(values)
                    .returnValues(ReturnValue.ALL_NEW)
                    .build();

            UpdateItemResponse response = ddb.updateItem(request);
            System.out.println(response.attributes()); // the item after the update
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

Explanation

  • AttributeValue.builder().n("1994") takes a String, and so does the shorter AttributeValue.fromN("1994"). There is no n(int) overload, because DynamoDB numbers hold 38 significant digits and no Java primitive does. Reading back, attributes().get("Awards").n() is a String too; the accessor for the wrong type returns null rather than throwing, so .s() on a number is a silent null, and .type() tells you which one is set.

  • response.attributes() — never null. With ReturnValue.NONE it returns a DefaultSdkAutoConstructMap that is empty but non-null, so a null check never fires and an isEmpty() check cannot distinguish "the service sent nothing" from "the item has no attributes". The generated hasAttributes() is the accessor that knows the difference. Every collection member in this SDK has one.

  • The builder type-checks everything except the part that matters. updateExpression(String) accepts any string; the compiler cannot tell SET from a typo, so expression mistakes are runtime 400s. ADD #upd2 :updValue2 is the atomic increment, a conditionExpression of attribute_exists(Artist) makes the call update-only, and the grammar is in update expressions.

  • Legacy attributeUpdates map — prefer the expression over it. Older examples still show it; it cannot express multiple clause types, aliases, or a condition in one request.

  • getMessage() is not the service's message. The SDK appends its own transport detail:

    The conditional request failed (Service: DynamoDb, Status Code: 400, Request ID: d99b117c-edd6-4dc9-8d3a-a5fa4fe9666c) (SDK Attempt Count: 1)

    Log that if you want the request ID for a support case. Compare on awsErrorDetails().errorCode() instead, and use awsErrorDetails().errorMessage() when you want the bare string.

Catch order matters more than usual here

ConditionalCheckFailedException extends DynamoDbException, so a catch (DynamoDbException e) placed first swallows the one failure you almost certainly wanted to branch on. Catch the specific type first, and take the item while you are there:

} catch (ConditionalCheckFailedException e) {
    // with .returnValuesOnConditionCheckFailure(ReturnValuesOnConditionCheckFailure.ALL_OLD)
    if (e.hasItem()) {
        Map<String, AttributeValue> loser = e.item();  // the item as it actually was
    }
} catch (DynamoDbException e) {
    // everything else
}

e.retryable() is false on this one, which is correct: retrying a failed condition just fails again.

The asymmetry to remember is that ValidationException has no class in this SDK. Search dynamodb-2.35.9.jar and there is nothing to catch. A reserved word, a malformed expression, a partial key: all of them arrive as a plain DynamoDbException whose awsErrorDetails().errorCode() happens to read ValidationException. In a statically typed language that is a jarring gap, and it means expression mistakes are runtime string comparisons.

Which is why DynamoDB's reserved words deserve a pass before you ship rather than after: the list runs to 573 entries and includes Year, Name and Status, none of which look dangerous in a Java bean. To browse the raw table rather than the bean mapping over it, download DynoTable.

The enhanced client cannot express this

If the rest of your data access goes through DynamoDbEnhancedClient and annotated beans, this operation is the one that drops you back to DynamoDbClient. Reflecting over UpdateItemEnhancedRequest.Builder turns up item, conditionExpression, ignoreNulls, ignoreNullsMode, returnValues, returnValuesOnConditionCheckFailure, returnConsumedCapacity and returnItemCollectionMetrics. There is no method that accepts an update expression.

The practical consequence is the atomic counter. ADD #upd2 :updValue2 increments Awards server-side with no read first; the enhanced client gives you a mapped bean and ignoreNulls to decide whether absent fields are removed, and nothing that compiles down to ADD. Read-modify-write through a bean is a lost-update race under concurrency, which is precisely what this page's snippet avoids.

References

Last verified 2026-07-28 against the official AWS documentation linked above.

Work with DynamoDB without the Console

A fast DynamoDB desktop client that runs the real SQL DynamoDB can’t — JOINs, GROUP BY, aggregates — with visual editing and an AI agent on your own Bedrock keys.

Free 30-day trial, no credit card — then the Free plan with no time limit.