Front-End Web & Mobile

Introducing server-side caching item eviction for AWS AppSync

AWS AppSync is a managed serverless GraphQL service that makes it easy to securely connect to data sources such as Amazon DynamoDB tables, AWS Lambda functions, and more. AppSync offers managed server-side caching that reduces the need to fetch data from your data sources for every single request. This lets developers optimize their GraphQL APIs, and helps return data to their clients with lower latency. Today, we are announcing a new capability to evict specific entries from AppSync’s server-side cache. Now, developers can implement functionality in their resolvers to target specific items in their cache, evict them, and make sure their requests fetch data from their backend to keep their cache fresh.

In this post, we will look at how developers can leverage this new functionality.

Getting started

Enabling server-side caching with AppSync is simple. We can turn on the feature by heading to the AWS console for AppSync, and selecting Caching in the right-side menu. From there, we turn on “Per-resolver caching”, and, in Cache Settings, configure the feature.

API cache page. "Per-resolver caching" selected under "Caching behavior. Cache settings selected are "cache.large" instance type, "60" cache time to live (TTL), "Encryption in transit and "Encryption at rest" enabled.

“Per-resolver caching” lets you choose the resolver for which you want to cache the response. You can use the default keys generated by AppSync, or you can specify values from the $context.arguments, $context.source, and $context.identity maps to build your custom caching key for each resolver. This gives you the flexibility to cache resolvers based on your application needs. Since evicting an entry requires that you know the exact value of the attributes used to build your key, we recommend using per-resolver caching with your specified keys if you plan to evict items.

In a previous post, we implemented a system to support a school that is offering classes to thousands of students. Here is the schema:

type Class @aws_api_key @aws_cognito_user_pools {
  id: ID!
  name: String!
  registrations: RegistrationConnection
}

type Registration @aws_cognito_user_pools {
  id: ID!
  name: String
  major: String @aws_cognito_user_pools(cognito_groups: ["admins", "students"])
}

type RegistrationConnection @aws_cognito_user_pools {
  items: [Registration!]
  nextToken: String
}

type Query @aws_api_key @aws_cognito_user_pools {
  getRegistrations: [Registration] @aws_cognito_user_pools(cognito_groups: ["students"])
  allClasses(semester: String!): [Class]
  getClass(id: ID!, limit: Int): Class @aws_cognito_user_pools(cognito_groups: ["admins", "instructors"])
  getMoreRegistrations(courseId: ID!, nextToken: String!, limit: Int): RegistrationConnection
    @aws_cognito_user_pools(cognito_groups: ["admins", "instructors"])
}

Because this is a read-heavy system where the class information doesn’t often change after the start of the school year, we can take advantage of server-side caching to improve the query latency and offload the backend datasource. We use the CLI to enable caching on the allClasses query by using the configuration in a file resolver-update.json:

{
    "apiId": "<API_ID>",
    "typeName": "Query",
    "fieldName": "allClasses",
    "dataSourceName": "registrations",
    "requestMappingTemplate": "<TEMPLATE>",
    "responseMappingTemplate": "<TEMPLATE>",
    "kind": "UNIT",
    "cachingConfig": {
        "ttl": 3600,
        "cachingKeys": [ "$context.arguments.semester" ]
    }
}

We update the resolver by using the AWS CLI and the update-resolver operation:

aws appsync update-resolver --cli-input-json file://resolver-update.json

Notice that we set the caching keys to "$context.arguments.semester", thereby caching the request for each different semester based on that value. We’ll use the new extensions utility to evict the cache entries any time that the class information for a semester is updated. First, we introduce a new mutation to update a class:

type Mutation @aws_cognito_user_pools {
  updateClassInformation(id: ID!, name: String!, semester: String): Class
}

Then, we attach the field resolver to the DynamoDB data source. We define a request and a response resolver. Next, we clear the entry from the cache in the response mapping template, as shown in the following:

#set($cachingKeysMap = {})
$util.qr($cachingKeysMap.put("context.arguments.semester", $context.arguments.semester))
$extensions.evictFromApiCache("Query", "allClasses", $cachingKeysMap)

$utils.toJson($context.result)

We use the new evictFromApiCache extensions utility to evict an entry from the cache. First, we provide a map (here named cachingKeysMap) where the map keys match the keys provided in the targeted resolver’s cachingKeys list, and the map values are the actual values to be used for the caching keys. The map entries must be added in the map in the same order as they are defined in the cachingKeys array. Finally, we call $extensions.evictFromApiCache and specify the type name, field name, and the keys map. When the mutation is called and the resolver runs, if an entry is successfully cleared, then the response contains an apiCacheEntriesDeleted value in the extensions object that shows how many entries were deleted.

"extensions": {
  "apiCacheEntriesDeleted": 1
}

The next time that an application does an allClasses query, AppSync will fetch the data from the DynamoDB table and update the cache with the new values. Note that you can only call $extensions.evictFromApiCache in a mutation resolver. The call can be made from the request and/or the response mapping template. You can call this utility multiple times in the resolver to evict multiple cache entries. Also note that your keys can be made of multiple values. For example, caching keys for a query that returns a student’s classes for a semester could look like this:

"cachingKeys": [ "$context.identity.username", "$context.arguments.semester" ]

A mutation called by a backend process is used to clear cache entries:

type Mutation {
  updateRegistration(username: String!, semester: String!): Registration @aws_iam
}

Eviction is done in the mutation’s response template:

#set($cachingKeysMap = {})
$util.qr($cachingKeysMap.put("context.identity.username", $context.arguments.username))
$util.qr($cachingKeysMap.put("context.arguments.semester", $context.arguments.semester))
$extensions.evictFromApiCache("Query", "getNote", $cachingKeysMap)

$utils.toJson($context.result)

Note that we add the caching keys to cachingKeysMap in the same order they are specified in the cachingKeys definition of the Query.getNote resolver. If we only need to clear the cache entry in response to a change that was made outside of AppSync, then we can attach the resolver to a NONE data source. We can control access to a mutation that evicts items by using AppSync’s field-level authorization directives. In the above example, we use the @aws_iam directive to restrict access to our backend process that has the permissions to complete the action.

Conclusion

Prior to this feature, developers could not target specific items for eviction, and they had to flush the entire server-side cache to make sure that their applications were never receiving stale data. Support for item eviction now lets developers have the flexibility to implement the caching scheme that they need. Furthermore, they can rest assured that their server-side cache in AppSync is always fresh. To find out more about the AppSync server-side caching feature, visit the documentation.

About the author

Brice Pellé

Brice Pellé is Principal Solution Architect working on AWS AppSync.