DynamoDB Query in Java (AWS SDK v2)

queryPaginator in AWS SDK for Java 2.x looks like a collection and is not one. It is a lazy re-iterable, and the difference shows up on your bill the second time you loop over it. For when Query is the right read at all, see Query vs. Scan.

Code

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;

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

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

            Map<String, AttributeValue> values = new HashMap<>();
            values.put(":hashKeyValue", AttributeValue.builder().s("Arturo Sandoval").build());
            values.put(":rangeKeyValue", AttributeValue.builder().s("C").build());

            QueryRequest request = QueryRequest.builder()
                    .tableName("Music")
                    .keyConditionExpression(
                            "#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)")
                    .expressionAttributeNames(names)
                    .expressionAttributeValues(values)
                    .build();

            List<Map<String, AttributeValue>> items = new ArrayList<>();
            for (QueryResponse page : ddb.queryPaginator(request)) {
                items.addAll(page.items());
            }
            System.out.println("Found " + items.size() + " items");
        } catch (DynamoDbException e) {
            System.err.println(e.getMessage());
        }
    }
}

QueryIterable re-runs the query every time you iterate it

Against a 600-song fixture, every song ~3.9 KB and all under Artist = "Arturo Sandoval", ddb.queryPaginator(request) returned in 2.6 ms and sent nothing. Then the same QueryIterable object was looped over twice:

queryPaginator(request) returned in 2.60 ms   (class QueryIterable, 0 requests)
iteration 1: pages=3  items=600  capacity=284.5
iteration 2: pages=3  items=600  capacity=284.5   <- same object

Two for loops over one variable, 569 read units. The pages were never cached; each iteration walks LastEvaluatedKey from the start again. If you need the items twice, drain the iterable into a List once, as the example does.

The flip side of the same laziness is where errors surface. Build a request whose key condition omits the partition key and queryPaginator accepts it without complaint, because no call has happened yet:

queryPaginator(bad) constructed without throwing
threw on iteration: DynamoDbException / ValidationException /
Query condition missed key schema element / http 400

A try-catch wrapped around the builder catches nothing. It has to wrap the loop, which is why the example puts the whole block inside one try.

Measured 2026-07-28 against DynamoDB Local (amazon/dynamodb-local) with software.amazon.awssdk:dynamodb 2.49.4 on OpenJDK 26.0.1.

Explanation

  • ddb.queryPaginator(request).items() flattens the pages into an Iterable<Map<String, AttributeValue>> and paginates behind you, so the two-level loop in the example collapses to one when you only want items. It is also a SdkIterable, so .stream() works.
  • Numbers are String in the builder. AttributeValue.builder().n("1994") is not a typo for .n(1994) — the n() setter takes a java.lang.String, because DynamoDB transports numbers as decimal text to avoid binary float rounding. Passing a Java int will not compile.
  • keyConditionExpression takes an equality on the partition key plus at most one sort-key condition (=, <, <=, >, >=, BETWEEN, begins_with); .scanIndexForward(false) reverses the order and .indexName("...") retargets a secondary index.
  • The Enhanced Client is the other ergonomic. software.amazon.awssdk.enhanced.dynamodb maps annotated beans instead of Map<String, AttributeValue>, and its query returns a PageIterable<T> with the same lazy re-iteration semantics measured above.

Do it visually

Both #hashKey and #rangeKey in this example are aliases nothing forces on you: neither Artist nor SongTitle is on AWS's 573-word reserved list. The reserved-words checker tells you which of your attribute names genuinely need the # treatment, so the alias map stops being cargo cult.

To try a key condition against a real table before you build the QueryRequest, download DynoTable and page through the results in a grid.

References

Build this request visually

Compose this operation in the free DynamoDB Query Builder — key condition, filter, index, Limit, sort order, and a pagination loop — and copy it back as a runnable SDK v3, CLI, or boto3 program.

Open the DynamoDB Query Builder

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.