Asynchronous DynamoDB Queries with Java Using GSIs and the AWS SDK v2 Enhanced Async Client
When building modern Java applications that interact with AWS DynamoDB, you often need to query data by attributes other than the primary key. This is where Global Secondary Indexes (GSIs) come into play.
In this post, we’ll break down:
- Why GSIs are important
- How to use the Enhanced Async DynamoDB Client in Java
- How to query a GSI asynchronously using Reactive Streams
- How to process results efficiently using CompletableFuture
1. Understanding Global Secondary Indexes (GSIs)
A Global Secondary Index (GSI) allows you to query DynamoDB tables using an alternative partition key and optional sort key. Unlike the primary key of the table:
- A GSI can have different partition and sort keys.
- It provides fast queries for attributes you don’t want to scan.
- GSIs are eventually consistent by default, but you can request strong consistency if needed.
Why use a GSI here?
In our scenario, we have a table of form templates with this schema:
templateId
→ primary keyownerId
→ GSI partition keytemplateName
→ GSI sort keyorgId
→ attribute for filtering
We want to find a template by ownerId + templateName, which is not the table’s primary key, so a GSI is perfect. It allows efficient lookups without scanning the whole table.
2. Using the Enhanced Async Client
First, create the async DynamoDB client:
DynamoDbAsyncClient asyncClient = DynamoDbAsyncClient.builder()
.region(Region.US_EAST_1)
.build();
Wrap it with the Enhanced Async Client:
DynamoDbEnhancedAsyncClient enhancedAsyncClient = DynamoDbEnhancedAsyncClient.builder()
.dynamoDbClient(asyncClient)
.build();
Access your table:
DynamoDbAsyncTable<Template> templateTable =
enhancedAsyncClient.table("templates", TableSchema.fromBean(Template.class));
3. Querying a GSI Asynchronously
To query the TemplateQueryIndex:
DynamoDbAsyncIndex<Template> gsi = templateTable.index("TemplateQueryIndex");
QueryConditional queryConditional = QueryConditional.keyEqualTo(
Key.builder()
.partitionValue(ownerId) // GSI partition key
.sortValue(templateName) // GSI sort key
.build()
);
This sets up a query that directly targets the GSI rather than scanning the table.
4. Reactive Streams: Processing Pages Asynchronously
The query()
method returns a Publisher of Pages, not a List. Each Page<Template>
may contain multiple items.
Publisher<Page<Template>> pagesPublisher = gsi.query(r -> r.queryConditional(queryConditional));
We subscribe to the publisher and accumulate items:
CompletableFuture<List<Template>> futureItems = new CompletableFuture<>();
List<Template> itemsList = new ArrayList<>();
pagesPublisher.subscribe(new Subscriber<Page<Template>>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Page<Template> page) {
itemsList.addAll(page.items());
}
@Override
public void onError(Throwable throwable) {
futureItems.completeExceptionally(throwable);
}
@Override
public void onComplete() {
futureItems.complete(itemsList);
}
});
5. Returning a CompletableFuture
return futureItems.thenApply(items ->
items.stream()
.filter(t -> orgId.equals(t.getOrgId()))
.findFirst()
.map(Template::getTemplateJson)
.orElse(null)
);
This filters by orgId
after the GSI query and returns templateJson
as a CompletableFuture<String>
.
6. Why This Approach Works
- Non-blocking: No threads are blocked while waiting for DynamoDB.
- GSI-efficient: Avoids scanning the whole table for lookups.
- Reactive: Works seamlessly with other reactive frameworks.
- Composable: Use
thenApply
,thenCompose
,whenComplete
to chain async operations.
7. Key Takeaways for Developers
- GSIs enable fast queries on non-primary key attributes.
- Enhanced Async Client is the modern approach for async queries.
- Reactive Streams integration ensures items are processed as they arrive.
- CompletableFuture wraps results for easy Java async composition.
- Always distinguish between Pages (units returned by
query()
) and items.
8. Conclusion
Using GSIs with the Enhanced Async DynamoDB Client lets you build high-performance, non-blocking applications that can efficiently query data based on secondary keys. Understanding Reactive Streams and the CompletableFuture
pattern is key to leveraging this capability in Java.
With this approach, developers can avoid table scans, handle large result sets asynchronously, and write clean, scalable DynamoDB code.
Comments
Post a Comment