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 shapeRound tripsRead unitsItems returned
as written3284.58
.limit(25)25288.08

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 400

Year 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. filterExpression runs after the read, so those two empty responses cost full price, and any if (page.items().isEmpty()) break reports an empty table.
  • ddb.scanPaginator(request).items() flattens the pages into a single Iterable<Map<String, AttributeValue>> and pages behind you, which collapses the nested loop when you only want items. It is an SdkIterable, so .stream() works — .items().stream().count() returns 8 here.
  • ScanIterable re-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 a List once, as the example does.
  • Numbers are String in the builder. AttributeValue.builder().n("2010") takes a java.lang.String, because DynamoDB transports numbers as decimal text. Passing an int will 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.

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.