AppSync Resolver Optimization by Removing Unnecessary GetItem's

AppSync Resolver Optimization by Removing Unnecessary GetItem's

I had a front end developer comment that one of API's was slow. The developer was trying download all of the products and their related image. After a quick investigation it turned out we had hit the dreaded N+1 problem.

Using AppSync's new context info object we've been able to apply a simple optimization that has significantly reduced the number of DynamoDB queries our API was making.

A simplified version of the schema would look something like this:

type Image {
  id: ID!
  url: String
}

type Product {
  id: ID!
  name: String
  image: Image
}

type Query {
  listImages: [Image]
  listProducts: [Products]
}

The AppSync resolvers for Query.listProducts and Query.listImages both performed a DynamoDB Query allowing one request to return multiple records. The Product.image resolver used a GetItem to fetch the image for each product.

In my head the developer would use a query like this

query ListProductsWithImage {
  listProducts {
    id
    name
    image {
      id
      url
    }
  }
}

You might notice a problem here. While listProducts can be performed with a single DynamoDB operation I'm performing another DynamoDB operation for every record that it returns. The N+1 problem.

It turned out that the developer was actually using two queries and joining the data in the application. The first downloaded all of the images

query ListImages {
  listImages {
    id
    url
  }
}

The second all of the products

query ListProducts {
  listProducts {
    id
    name
    image {
      id
    }
  }
}

While this could have been efficient it wasn't because the Product.image resolver was still performing a GetItem for each product returned by Query.listProducts.

This was crazy because we store the Image id in the imageId attribute as part of the Product record in DynamoDB.

One solution would be to expose the imageId in Product but this is discouraged in GraphQL schema design because it exposes the internal implementation details to an external client making it harder to change the implementation later.

With the new $ctx.info object and early returning I can now optimize my resolvers for this use case by adding the following code at the top of the Product.image request resolver.

#if ($ctx.info.selectionSetList.size() == 1 && $ctx.info.selectionSetList[0] == "id")
  #return({ "id": "$ctx.source.imageId" })
#end

If id is the only image field requested it will now return early, before the GetItem operation, using $ctx.source.imageId as the id.

This small optimization improves API performance and saves money by removing unnecessary GetItem requests without exposing the internal implementation but still allows the client to fetch additional fields if required.