DynamoDB PutItem in Java (AWS SDK v2)

PutItem writes a whole item and replaces any existing item with the same primary key (item-based actions covers how that differs from UpdateItem). In AWS SDK for Java 2.x every attribute goes into a PutItemRequest as a typed AttributeValue, and the builder will let you construct one that cannot possibly be valid.

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.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;

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

            Map<String, AttributeValue> item = new HashMap<>();
            item.put("Artist", AttributeValue.builder().s("Arturo Sandoval").build());
            item.put("SongTitle", AttributeValue.builder().s("Cubano Chant").build());
            item.put("AlbumTitle", AttributeValue.builder().s("Danzon").build());
            item.put("Year", AttributeValue.builder().n("1994").build());
            item.put("Awards", AttributeValue.builder().n("0").build());

            Map<String, String> names = new HashMap<>();
            names.put("#cond0", "Artist");
            names.put("#cond1", "SongTitle");

            PutItemRequest request = PutItemRequest.builder()
                    .tableName("Music")
                    .item(item)
                    .conditionExpression("attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)")
                    .expressionAttributeNames(names)
                    .build();

            try {
                ddb.putItem(request);
                System.out.println("Song written");
            } catch (ConditionalCheckFailedException e) {
                System.out.println("A song with that key already exists — not overwritten");
            }
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

Explanation

AttributeValue.builder().build() compiles. It is also unsendable. The builder has no required field, so an attribute where you forgot the .s(...) type-checks perfectly and fails at the service:

DynamoDbException | ValidationException | Supplied AttributeValue is empty, must contain exactly one of the supported datatypes

This is the Java-specific shape of a mistake other SDKs make impossible: Go's types.AttributeValueMember* are separate types, so there is nothing to leave unset. See "Supplied AttributeValue is empty" for the wider fix.

A Java null handed to .s(...) does not become a DynamoDB NULL. This is the version that actually bites, because it looks like a value:

AttributeValue.builder().s(customer.getNotes()).build()   // getNotes() returned null

No NullPointerException is thrown at construction. The builder simply records nothing, and the request fails with the identical Supplied AttributeValue is empty message, pointing at an attribute you never suspected. If you want a real null, that is AttributeValue.builder().nul(true).build(); more often you want to omit the entry. Note this is the opposite of the Go SDK, where a nil pointer marshals to NULL and silently creates an attribute; both were run against the same engine on the same day.

getMessage() is not the service message. The SDK appends its own context, so the string is:

The conditional request failed (Service: DynamoDb, Status Code: 400, Request ID: 77a08ef1-a3a9-4f97-b309-4cb0741edd1a) (SDK Attempt Count: 1)

Match on e.awsErrorDetails().errorCode() and read e.awsErrorDetails().errorMessage() for the bare text. Anything comparing getMessage() against a literal is broken by a retry, which changes the attempt count.

Catch ConditionalCheckFailedException before DynamoDbException, and read what it carries. It extends DynamoDbException, so ordering the catch blocks the other way round makes the specific handler unreachable. On the caught object: statusCode() returned 400 and retryable() returned false, which is the honest answer for a business-logic rejection. Add .returnValuesOnConditionCheckFailure("ALL_OLD") to the request and e.item() comes back populated with the item that blocked the write (five attributes in the run above, Year as AttributeValue(N=1994)), so you do not need a follow-up getItem to find out who won.

Numbers go in as strings through .n(...). DynamoDB's N type is decimal text on the wire, which is what keeps 1994 from becoming a double. .n(String.valueOf(year)) is the idiom; there is no .n(int) overload to reach for.

The client is Closeable, and long-lived. The try-with-resources above is right for a one-shot program and wrong for a service: DynamoDbClient owns an HTTP connection pool and is thread-safe, so build one per application and let it live. Building one per request is the most common Java performance bug on this API.

Prefer beans to AttributeValue maps? The DynamoDB Enhanced Client (software.amazon.awssdk.enhanced.dynamodb) maps an annotated class straight to an item, which removes the empty-builder trap entirely. It costs a reflective TableSchema.fromBean scan at startup, which StaticTableSchema avoids if that matters to you.

Do it visually

Every failure above starts with hand-built typed values. The free DynamoDB JSON converter takes ordinary JSON and returns the typed form, so you can see exactly what the item should look like on the wire before writing a single AttributeValue.builder().

To write and edit items against your own tables — a form per attribute, type pickers, copy the result back out as Java — download DynoTable.

References

Reproduced 2026-07-28 with AWS SDK for Java 2.49.4 on OpenJDK 26.0.1, against DynamoDB Local (amazon/dynamodb-local) on port 9000. The exception text, status code and item contents above are captured output, copied verbatim.

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.