Skip navigation

Improve performance with caching

13 min read
Download PDF

Make your apps more responsive, resilient, and generally perform better with caching. Caching helps eliminate the wait time that you and the users of your app would otherwise experience when your app gets content from an API or experiences temporary connection issues.

Table of contents

    Cache in a nutshell

    Cache is an additional layer of storage used for quickly retrieving data within your application. In the context of Kontent, cache sits between your app's business layer (that is the logic to perform common actions) and the Delivery API. Cache contains cache entries with responses from the Delivery API that are referenced by a key. The key corresponds to an action, for example, GetPost|about_us. When the application needs to fetch content, it first checks if the cache contains a response with the specified key. If the response is available in the cache, there is no need to send a request to the Delivery API. If the response is not available in the cache, the application needs to make an API request and store the result in the cache.

    In case the cache entries need to be invalidated, for example, when your app receives a webhook notification about a change in published content, the cache entries must specify cache dependencies. When your app puts an API response in the cache, it also specifies dependencies on dummy cache entries (that is special cache entries without any value). Once your app receives a webhook notification, it invalidates relevant dummy cache entries. As a result, the responses that depend on these dummy cache entries are invalidated as well.

    For example, a cache entry holds a JSON response from the Delivery API. If you're using webhooks and the API response contains a single content item, the cache needs to know about the dependency between the JSON response and the object it contains. The object, in this case a content item, is a dependency of the JSON response. Once your app is notified that the object has changed, your app invalidates the cache dependency and removes the cache entry from cache. On the next request, your app retrieves an updated JSON response from the Delivery API and adds new cache entries.

    A diagram showing cache entries and their dependencies.

    Cached responses and their dependencies on Kontent objects.

    Before you start adding caching logic to your app, decide whether you need to use webhook notifications.

    Choosing your approach

    For most applications, we recommend that you build your app's cache layer without any webhook logic for two reasons. First, it's much easier to implement. Second, it performs better because your app doesn't need to invalidate cache entries every time there's been a change in the project. One way to implement the no-webhooks approach is to set cache expiration to a specific amount of time. The amount of time should correspond to your users' expectations and their tolerance to content that hasn't been updated for, say, 10 minutes. For example, if you have two articles tagged with a specific tag and then you rename the tag, your app will refresh the two articles in cache after each article expires on its own.

    If you need to display content that is always up to date, you need to use webhooks. Whenever your app receives a notification about changes in published content, it needs to invalidate specific items in its cache. Your app can use an expiration mechanism of your choice, such as the time-based caching mentioned earlier, but the webhook notification will override the expiration time.

    Caching without webhooks

    The following scenario is built on the example of a server-side web application. The web application powers a blog site, which can show a list of blog posts, display a detail of a specific blog post, organize blog posts by tags, and render navigation. Combined with Kontent, each blog post represents a content item and each tag represents a taxonomy term.

    Define your app's business layer

    It is a good practice to define specific actions within your app (such as retrieving a blog post) and rely on those custom actions throughout your app. Think about the actions that your app performs regularly and create methods for them.

    Using the blog site example, these actions can be:

    • Get a blog post
    • Get a list of blog posts
    • Get a list of blog posts by category

    In pseudocode, the actions might look like this.

    • JavaScript
    // Using the Delivery client directly: Client = DeliveryClient("<YOUR_PROJECT_ID>") response = Client.items() .type("<type_codename>") .limit(10) .skip(20) // Using your custom actions: // Retrieves a blog post by its codename GetPost("<post_codename>") // Retrieves a list of blog posts, also used for paging GetPosts( <skip>, <limit>) // Retrieves a list of blog posts tagged with a specific category, also used for paging GetPostsByCategory("<category_codename>", <skip>, <limit>)
    // Using the Delivery client directly: Client = DeliveryClient("<YOUR_PROJECT_ID>") response = Client.items() .type("<type_codename>") .limit(10) .skip(20) // Using your custom actions: // Retrieves a blog post by its codename GetPost("<post_codename>") // Retrieves a list of blog posts, also used for paging GetPosts( <skip>, <limit>) // Retrieves a list of blog posts tagged with a specific category, also used for paging GetPostsByCategory("<category_codename>", <skip>, <limit>)

    One of the benefits of using your own custom actions is that you can use a combination of the action's name and input parameters to name the response (in other words, compose a cache entry key) and store it in the cache.

    Implement logic for caching your content

    When you retrieve a blog post using your custom GetPost() action, you get a JSON response with the specified blog post (content item) and possibly several other content items linked to the specified blog post. In simple terms, whenever you get a response from the API, you store it in the cache. To do this, create a cache entry that uses a naming pattern such as <action_name>|<action_parameters> for its key. Using this pattern you can uniquely identify the responses within the cache for each of your custom actions.

    For instance, if you're retrieving a blog post named My blog post (which translates into calling GetPost("my_blog_post")), the cache entry key for the response will be named GetPost|my_blog_post. You can adjust the pattern to suit your needs and naming conventions.

    Once you put the response in cache, you're done. The next time your app calls GetPost("my_blog_post"), it will quickly retrieve the blog post from cache without making any request to the API.

    Invalidating cache with webhooks

    If your approach requires webhooks, you will need to add more advanced logic to your app so that it can identify dependencies in the responses and invalidate cache entries based on information in the webhook notifications.

    Identify dependencies in responses

    For example purposes, let's assume the response (that is the retrieved blog post) contains a few components and links to several content items. The linked items are dependencies of the blog post. If the dependencies change, so should the blog post. Let's now go through how you can identify the dependencies and store them in cache.

    The structure of the response will look similarly to the simplified JSON below.

    • The blog post itself is within the item object.
    • Components and linked items are within the modular_content collection as separate objects.
    • JSON
    { "item": { "system": { "id": "f4b3fc05-e988-4dae-9ac1-a94aba566474", "name": "My blog post", "codename": "my_blog_post", "language": "default", "type": "blog_post", "sitemap_locations": [], "last_modified": "2019-10-20T12:03:48.4628352Z" }, "elements": { ... } }, "modular_content": { "other_blog_post": { "system": { ... }, "elements": { ... } }, "n2dfcbed2_d7a1_0183_4324_a2282f735f48": { "system": { ... }, "elements": { ... } } } }
    { "item": { "system": { "id": "f4b3fc05-e988-4dae-9ac1-a94aba566474", "name": "My blog post", "codename": "my_blog_post", "language": "default", "type": "blog_post", "sitemap_locations": [], "last_modified": "2019-10-20T12:03:48.4628352Z" }, "elements": { ... } }, "modular_content": { "other_blog_post": { "system": { ... }, "elements": { ... } }, "n2dfcbed2_d7a1_0183_4324_a2282f735f48": { "system": { ... }, "elements": { ... } } } }

    You need to go through the response, find any content items, and add them as dependencies for the cached JSON response.

    Identifying content items in modular_content

    When you retrieve content items from the Delivery API, the modular_content collection contains both content items and components. Structurally, components and content items look the same in the API response.

    To identify content items within the response, find the object's ID and check the third group of characters in the ID.

    • If the characters do NOT start with 01, for example, ce0288e7-294c-46e5-b9bc-b086656d5c48, it's a content item.
    • If the characters start with 01, it's a component.

    To uniquely identify the cache dependencies, use a naming pattern such as <object_type>:<object_codename> . For example, using the content item from the simplified JSON, the dependency would be named item:other_blog_post. In this way you can invalidate the correct dependencies whenever you receive a notification about changes in content items.

    The same principle applies to other types of responses that contain a list of items or taxonomy groups. Use the following guidelines for constructing the cache dependency logic in your app.

    For response withAdd cache dependencies forExplanation
    Single content itemCodenames of one or more items
    OR –
    Any item
    Add cache dependencies for each content item object found in the response, if the number of items is small (up to 30 items). This includes the requested item itself and any items in the modular_content collection.

    If the response contains too many content items to cache separately (for example, over 30 items), add a special cache dependency to any content item. In other words, if any content item changes, so should the cached response.
    List of content itemsAny itemIf any content item in the cache is invalidated, the cache entry with a list of content items needs to be invalidated as well.
    Single taxonomy groupCodename of one taxonomy groupAdd a cache dependency for the taxonomy group object returned within the response.
    List of taxonomy groupsAny taxonomy groupIf any single taxonomy group in the cache is invalidated, the cache entry with a list of taxonomy groups needs to be invalidated as well.

    Once you're done adding the logic, you need to specify when each dependency should be invalidated.

    Invalidate cache entries based on webhook notifications

    Webhook notifications from Kontent consist of two objects, Data and Message. The Data object specifies which entities in your project changed. The Message object tells you why the notification came and contains additional metadata about the notification. See webhooks reference to learn more about the notifications.

    • JSON
    { "data": { "items": [ { "id": "e5d575fe-9608-4523-a07d-e32d780bf92a", "codename": "other_blog_post", "language": "default", "type": "blog_post" } ], "taxonomies": [ { "id": "4794dde6-f700-4a5d-b0dc-9ae16dcfc73d", "codename": "tags" } ] }, "message": { ... } } }
    { "data": { "items": [ { "id": "e5d575fe-9608-4523-a07d-e32d780bf92a", "codename": "other_blog_post", "language": "default", "type": "blog_post" } ], "taxonomies": [ { "id": "4794dde6-f700-4a5d-b0dc-9ae16dcfc73d", "codename": "tags" } ] }, "message": { ... } } }

    To use the notifications for cache invalidation, your app needs to go through the arrays of items and taxonomies in the Data object. Each array contains objects that specify the modified content item or taxonomy group. Using these objects' codenames, you can construct identifiers of the specific dependencies and invalidate them.

    For example, if you receive a notification with an object in the items array and that object's codename is other_blog_post, you'll invalidate a cache dependency identified as item:other_blog_post. This in turn invalidates any JSON responses dependent on the modified content item.

    Use the following guidelines for specifying which cache dependencies must be invalidated after your app receives a webhook notification.

    For notification about changes inInvalidate dependency keys forExplanation
    Content itemsOne or more items
    AND Any item
    The notification tells you which content items were affected by a change in your project.

    Pair the specific content items with specific cache dependencies and invalidate them.

    Also, do the same for cached responses containing any list of items. For example, if you're caching paged responses, the removal of one content item from the first response would affect all of the following paged responses.
    Taxonomy groupsOne or more taxonomy groups
    AND Any taxonomy group
    AND Any item
    AND Any type
    The notification tells you which specific taxonomy group changed and which content items were affected.

    Pair the specific taxonomy groups with specific cache dependencies and invalidate them.

    Also, do the same for cached responses containing lists of any objects. Because taxonomy groups may be used in any content type and content item, you need to refresh any cached lists of these objects.
    Content typesAny typeThe notification tells you that a content type changed.

    If you're caching content types, you need to refresh all cached content types after receiving the notification.

    What's next?

    In this tutorial, you've learned how to decide whether you need webhooks in your app and how to cache and invalidate your content.