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 key
  • ownerId → GSI partition key
  • templateName → GSI sort key
  • orgId → 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

Popular posts from this blog

Building and Deploying a Fargate Container that runs Python and performs CloudWatch Logging

Automate Your API Gateway Setup with Boto3: Rendering HTML from Lambda

Setting up an AWS Cognito User Pool and building a React login component