DynamoDB Scan in Java (AWS SDK v2)
scanPaginator handles LastEvaluatedKey for you, and the one knob most people reach for next — .limit(...) — makes the same scan slower and slightly more expensive. For when to avoid Scan entirely, 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.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanResponse;
public class ScanExample {
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("#filter0", "Year");
Map<String, AttributeValue> values = new HashMap<>();
values.put(":filterValue0", AttributeValue.builder().n("2010").build());
ScanRequest request = ScanRequest.builder()
.tableName("Music")
.filterExpression("#filter0 >= :filterValue0")
.expressionAttributeNames(names)
.expressionAttributeValues(values)
.build();
List<Map<String, AttributeValue>> items = new ArrayList<>();
for (ScanResponse page : ddb.scanPaginator(request)) {
items.addAll(page.items());
}
System.out.println("Matched " + items.size() + " items");
} catch (DynamoDbException e) {
System.err.println(e.getMessage());
}
}
}.limit(25) turns 3 requests into 25 and costs more
The fixture holds 600 songs of roughly 3.9 KB each, and 8 of them match. Run the example as written, then run it again with .limit(25):
| Request shape | Round trips | Read units | Items returned |
|---|---|---|---|
| as written | 3 | 284.5 | 8 |
.limit(25) | 25 | 288.0 | 8 |
Capacity is billed per page, rounded up to a 4 KB boundary, so slicing one 1 MB read into 24 small ones pays the rounding 24 times. The 25th request is the other surprise: page 24 finished the table and still returned a LastEvaluatedKey, so the paginator asked once more and got scannedCount=0. DynamoDB signals "no more data" by omitting that key, not by returning a short page, and at a Limit boundary it does not know yet.
.limit(...) is a capacity-smoothing knob for a background job you do not want to throttle a live table. It is not a way to make a scan cheaper or shorter.
The alias is not stylistic
Drop #filter0 and filter on Year directly, and the SDK surfaces this through awsErrorDetails():
DynamoDbException / ValidationException /
Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year
/ http 400Year is one of 573 reserved words. Like every paginator error in this SDK, it arrives on the first iteration of the for loop, not when you call scanPaginator(request), so the try has to wrap the loop.
Measured 2026-07-28 against DynamoDB Local (amazon/dynamodb-local) with software.amazon.awssdk:dynamodb 2.49.4 on OpenJDK 26.0.1.
Explanation
- The first two pages return zero items. With this filter the per-page results are 0, 0 and 8, at 128.5, 128.5 and 27.5 read units.
filterExpressionruns after the read, so those two empty responses cost full price, and anyif (page.items().isEmpty()) breakreports an empty table. ddb.scanPaginator(request).items()flattens the pages into a singleIterable<Map<String, AttributeValue>>and pages behind you, which collapses the nested loop when you only want items. It is anSdkIterable, so.stream()works —.items().stream().count()returns 8 here.ScanIterablere-runs the scan on every iteration. It is lazy, not cached: looping over the same object twice sends the requests twice and bills twice. Drain it into aListonce, as the example does.- Numbers are
Stringin the builder.AttributeValue.builder().n("2010")takes ajava.lang.String, because DynamoDB transports numbers as decimal text. Passing anintwill not compile. .segment(...)/.totalSegments(...)split a full-table scan across workers, each with its own paginator. That divides wall-clock time and spends the same capacity.
Do it visually
The reserved-words checker runs your attribute names against the full AWS list and hands back the ExpressionAttributeNames map, which is faster than learning that Year, Name, Size and Status are all taken one ValidationException at a time.
To try a filter against a real table before you wire it into a ScanRequest, download DynoTable and page through the results in a grid.
Related examples
- DynamoDB Scan in Go — the same scan with AWS SDK for Go v2.
- DynamoDB Query in Java — the cheaper read you should usually reach for.
- Query vs. Scan — when (rarely) a
Scanis justified. - Why is my DynamoDB Scan slow and expensive? — the cost model and how to avoid it.
- DynamoDB ProvisionedThroughputExceededException — what a full-table scan does to a provisioned table's capacity.
- DynamoDB ThrottlingException — the other throttle, and how exponential backoff handles it.
References
- Scan — Amazon DynamoDB API Reference
- Use Scan with an AWS SDK or CLI — Amazon DynamoDB Developer Guide
- DynamoDbClient — AWS SDK for Java 2.x API Reference
- ScanRequest — AWS SDK for Java 2.x API Reference
- Scanning tables — Amazon DynamoDB Developer Guide
- Reserved words in DynamoDB — Amazon DynamoDB Developer Guide