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 objectTwo 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 400A 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 anIterable<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 aSdkIterable, so.stream()works.- Numbers are
Stringin the builder.AttributeValue.builder().n("1994")is not a typo for.n(1994)— then()setter takes ajava.lang.String, because DynamoDB transports numbers as decimal text to avoid binary float rounding. Passing a Javaintwill not compile. keyConditionExpressiontakes 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.dynamodbmaps annotated beans instead ofMap<String, AttributeValue>, and itsqueryreturns aPageIterable<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.
Related examples
- DynamoDB Query in Go — the same query with AWS SDK for Go v2.
- DynamoDB Scan in Java — when you can't key into a partition.
- Query vs. Scan — why
Queryis the right default. - Key condition expressions — every legal partition/sort-key operator.
- "Query condition missed key schema element" — the key condition names the wrong attribute or skips the partition key.
- "Query key condition not supported" — an operator the key condition can't use, like contains or a second sort-key condition.