Front-End Web & Mobile

Improving GraphQL API performance and consistency with AWS AppSync Caching and DynamoDB Transactions support

AWS AppSync is a managed GraphQL service that simplifies application development by letting you create a flexible API to securely access, manipulate, and combine data from one or more data sources. Different data sources are often optimized for different use cases and may deliver data at different speeds. The underlying data fields defined in the GraphQL schema can also be fairly diverse.

For example, in an e-commerce application, data fields representing inventory volumes are set to update often, customer profiles update occasionally, and transaction IDs are immutable. Directly accessing data sources for every data fetch operation adds to the overall system latency, which impacts the user experience and performance of applications. A caching layer returning data that changes less frequently can help to improve performance, decrease latency as well as optimize data source utilization since data that was previously requested can be served directly from cache for subsequent requests.

In other situations, such as processing financial transactions, fulfilling and managing orders, coordinating actions across distributed components and services, developers need to implement business logic that requires multiple all-or-nothing operations where applications might require coordinated inserts, deletes, or updates to multiple items as part of a single logical business operation when using Amazon DynamoDB as an AWS AppSync data source.

Today we are releasing two important features that will help developers address these different requirements with built-in support for server-side caching and Amazon DynamoDB transactions in AWS AppSync GraphQL APIs.

Caching

AppSync now provides built-in server-side caching capabilities for any supported data source, improving the performance of latency-sensitive and high-throughput applications and allowing developers to fetch data from a fast, in-memory, managed cache, delivering data at low latency.

With the new caching features, you can define different caching behaviors for your GraphQL API:

  • None: No server-side caching, this is the default behavior for AppSync APIs.
  • Full request caching: If the data is not in the cache, it’ll be retrieved from the data source, populating the cache until the TTL expiration. After that, all requests to your API will be returned from the cache, which means data sources won’t be contacted directly.
  • Per-resolver caching: Only API calls requesting data from a specific operation or field defined in a resolver will return responses from the cache.

After defining the appropriate caching behavior, you can select a cache instance type according to different memory and network performance requirements as well as define the amount of time cached entries will be stored in memory (time-to-live or TTL). Cache is fully managed by AppSync, as anything in the service you don’t need to worry about the undifferentiated heavy lifting of maintaining and operating infrastructure. You can also chose to encrypt the cache in transit and at rest (for data saved to disk from memory during swap operations):

With per-resolver caching you can customize how you want GraphQL calls to be cached and fit any requirement. Maybe there’s a specific type, a field in a type or a specific query operation in your GraphQL schema that should be cached however everything else should reach the data source directly.

For instance, take the following types and queries from the GraphQL schema created using the Event App sample project with the Create API wizard in the AppSync Console:

type Event {
    id: ID!
    name: String
    where: String
    when: String
    description: String
    comments(limit: Int, nextToken: String): CommentConnection
}

type Comment {
    eventId: ID!
    commentId: String!
    content: String!
    createdAt: String!
}

type EventConnection {
    items: [Event]
    nextToken: String
}

type CommentConnection {
    items: [Comment]
    nextToken: String
}

type Query {
    getEvent(id: ID!): Event
    listEvents(limit: Int, nextToken: String): EventConnection
}

There are 2 DynamoDB tables storing Events and Comments. In case the application needs to cache only the name of the events and nothing else, you just need to attach a resolver to the name field:

In the Create new Resolver screen select the data source where the name field is stored, in this case the DynamoDB table AppSyncEventTable:

Scrolling down to the Cache Settings section, select Enable Caching and define the TTL for the cached resolver then click Save Resolver to save the settings:

This will enable caching for the resolver you just created for the name field of the Event type. If I wanted to cache a query operation instead of a single field, for instance the getEvent query defined in the schema, the process is exactly the same. It’s just a matter of enabling the cache and define a custom TTL in the query resolver settings.

It’s also possible to create cache keys to ensure the uniqueness of the cached value of the data. Cache keys can be defined based on the user identity (context.indentity) or on arguments passed to the resolver (context.arguments) in the context object.

For instance, if my Events API authorized users based on OpenID Connect or Cognito User Pools and 2 different users are going to the same event I could use context.identity.sub (unique identifier for the user) and context.argument.id (unique identifier for the Event) as the cache keys to retrieve the cached data for the users accessing the same Event ID. In order to setup the cache keys as such, create a file named cacheConfig.json with the following content:

{
    "ttl": 1000,
    "cachingKeys": [
         "$context.identity.sub",
         "$context.arguments.id"
    ]
}

After enabling caching for the query, you can update the getEvent resolver with the desired cache keys using the following AWS CLI command that will refer to the cacheConfig.json file as well as files containing the existing request and response mapping templates for the query:

$aws appsync update-resolver --api-id 123456789example --type-name Query --field-name getEvent --caching-config file:///cacheConfig.json --request-mapping-template file:///Query.getEvent.req.vtl --response-mapping-template file:///Query.getEvent.res.vtl --data-source-name AppSyncEventTable 
{
    "resolver": {
        "typeName": "Query",
        "fieldName": "getEvent",
        "dataSourceName": "AppSyncEventTable",
        "resolverArn": "arn:aws:appsync:us-west-2:xxxxxxxxxxxx:apis/123456789example/types/Query/resolvers/getEvent",
        "requestMappingTemplate": ""{\n \"version\": \"2017-02-28\",\n \"operation\": \"GetItem\",\n \"key\": {\n \"id\": { \"S\": \"$context.arguments.id\" }\n }\n}",
        "responseMappingTemplate": "$util.toJson($context.result)",
        "kind": "UNIT",
        "cachingConfig": {
            "ttl": 1000,
            "cachingKeys": [
                "$context.identity.sub",
                "$context.arguments.id"
            ]
        }
    }
}

If cached data gets stale before the TTL expires, the cache can be easily flushed and invalidated by clicking the Flush cache button:

Finally, it’s also very important to easily understand how effective the caching is performing and if it’s bringing enough value and improvements to your GraphQL API. In order to provide proper visibility, all the cache activity for your AppSync API including cache hits, misses, evictions, bytes used and more can be conveniently monitored at any time from a single dashboard in the Monitoring section of your API in the console:

There is an additional charge to use caching on AppSync, it’ll be billed per hour without any long-term commitments until the cache is deleted from your GraphQL API. For more information visit our pricing page.

DynamoDB Transactions

In addition to caching, AppSync now also supports transactions for Amazon DynamoDB data sources and resolvers. DynamoDB Transactions simplify the developer experience of making coordinated, all-or-nothing changes to multiple items both within and across tables. Transactions provide atomicity, consistency, isolation, and durability (ACID) in DynamoDB, helping you to maintain data correctness in your applications.

AppSync now supports the two DynamoDB operations used to handle transactions in Resolvers:

  • TransactWriteItems, a batch operation that contains a write set, with one or more PutItem, UpdateItem, and DeleteItem operations.
  • TransactGetItems, a batch operation that contains a read set, with one or more GetItem operations.

Now it’s possible to have a GraphQL mutation that will issue a transaction to create or update items in different tables using something like the following VTL template in the mutation resolver:

{
        "version": "2018-05-29",
        "operation": "TransactWriteItems",
        "transactItems": [
           {
               "table": "posts",
               "operation": "PutItem",
               "key": {
                   "post_id": {
                       "S": "p1"
                   }
               },
               "attributeValues": {
                   "post_title": {
                       "S": "New title"
                   },
                   "post_description": {
                       "S": "New description"
                   }
               },
               "condition": {
                   "expression": "post_title = :post_title",
                   "expressionValues": {
                       ":post_title": {
                           "S": "Expected old title"
                       }
                   }
               }
           },
           {
               "table":"authors",
               "operation": "UpdateItem",
               "key": {
                   "author_id": {
                       "S": "a1"
                   },
               },
               "update": {
                   "expression": "SET author_name = :author_name",
                   "expressionValues": {
                       ":author_name": {
                           "S": "New name"
                       }
                   }
               },
           }
        ]
    }

Each transaction can include up to 25 unique items. If a transaction succeeds, the order of retrieved items will be the same as the order of request items. It’s also important to notice transactions are performed in an all-or-nothing way. If any requested item causes an error, the whole transaction will not be performed and the error details will be returned.

You can find more information and a handy tutorial on DynamoDB transactions with AppSync at the developer guide.

Conclusion

With powerful features such as caching and transactions support, AppSync provides more flexibility for GraphQL APIs requiring low latency with a managed caching layer or coordinated changes across different DynamoDB tables with atomicity, consistency, isolation, and durability (ACID) in NoSQL with transactions support.

What else would you like to see in AWS AppSync caching or DynamoDB integration? Feel free to create a feature request in our GitHub repository. Our team constantly monitors the repository and we’re always interested to hear developer feedback in order to make sure your application is successful with AppSync.