DocumentationLogin
Enterspeed logo

Automatic Cache Invalidation

Enterspeed patterns

There's an old computer science saying: there are only two hard things in Computer Science: cache invalidation and naming things. Naming things we can't help you with, but let's dig in to cache invalidation.

When you need to increase performance, the go-to strategy is caching. Caching can be a very efficient strategy, and most websites and web applications implement several caching techniques – both on the server-side and on the client-side. For this pattern, we are in the server-side context.

Caching is often used when data is fetched from a database or an API. Fetching data from a database or an API can be slow, either because the query is complicated or the load on the data source is high. To speed things up, you, as a developer, add a cache layer to temporarily store the result of any repeated calls to the same query or data source, so any subsequent visitors don't need to go to the source. Now you just need to decide when the cache should expire.

There are generally two strategies to cache invalidation:

1. Time-based invalidation

2. Event-based invalidation

In the time-based strategy, each cached item has a set lifespan, after which it's considered invalid, regardless of whether the original data has changed or not. This is a very simple approach, but it's always a trade-off between data freshness and the efficiency of the cache.

The event-based strategy is based on changes in the source data. These changes can be detected through various methods, such as polling the data source, subscribing to change events, or using database triggers.

How to implement this pattern

One way to implement event-based cache invalidation using a Write-Through strategy is the following. Write-Through Cache Invalidation involves writing changes to the cache and the main data store simultaneously.

Here's a step-by-step guide to implementing Write-Through Cache Invalidation:

Remember, the Write-Through strategy is beneficial in scenarios where reads are much more frequent than writes and where having the most up-to-date data is critical. However, it can be slower than other strategies for write-heavy workloads because every write operation needs to update both the cache and the main data store.

How to implement with Enterspeed

With Enterspeed, cache invalidation is built in. The first step is to send your data to Enterspeed. The following code snippet is a JavaScript example using our Ingest REST API, but you can also use our .NET Enterspeed Source SDK.

1// Ingest example in JavaScript using the Enterspeed Ingest REST API
2
3const ingestToEnterspeed = async (sourceEntity) => {
4  const url = `https://api.enterspeed.com/ingest/v2/${sourceEntity.id}`;
5  const response = await fetch(new Request(url), {
6    method: "post",
7    headers: {
8      "Content-Type": "application/json; charset=UTF-8",
9      "X-Api-Key": "[EnterspeedApiKey]",
10    },
11    body: JSON.stringify(sourceEntity)
12  });
13  return response.json();
14};
15
16const mySourceEntity = {
17  id: 12, 
18  url: 'https://mysite.com', 
19  type: 'contentPage', 
20  properties: { title: 'My headline' }
21};
22
23ingestToEnterspeed(mySourceEntity)
24  .then((data) => {
25    console.log(data);
26});

You then create the relevant schemas in Enterspeed. For this example, we'll go through 3 scenarios.

1:1 mapping

The first scenario is a simple 1:1 property mapping. Every time you send data to Enterspeed, the schema is triggered, and the data is updated in the cache and is immediately available in the Delivery API:

1// contentPage schema with a 1-1 property mapping
2
3/** @type {Enterspeed.FullSchema} */
4export default {
5  triggers: function(context) {
6    context.triggers('cms', ['contentPage'])
7  },
8  routes: function(sourceEntity, context) {
9    context.url(sourceEntity.url)
10  }
11  properties: function (sourceEntity, context) {
12    return sourceEntity.properties
13  }
14}

Referenced data

The second scenario is updating e.g. page titles and URLs on referenced data. Imagine you have a list of links in a top navigation. First you have the Navigation schema:

1// homePage schema with references to a list of navigation items
2
3/** @type {Enterspeed.FullSchema} */
4module default {
5  triggers: function(context) {
6    return context.triggers('cms', ['homePage'])
7  },
8  properties: function ({url, properties: p}, context) {
9    return {
10      navigationItems: context.reference('link').
11                                byOriginIds(p.navigationItems.map(x => x.id))
12    }
13  }
14}

Notice the .context.reference("link").children() method call. This inserts a reference to the view that's created by the Link schema and the front-page source entity's children.

1// link schema for multiple source entity types
2
3/** @type {Enterspeed.FullSchema} */
4module default {
5  triggers: function(context) {
6    return context.triggers('cms', ['homePage', 'contentPage'])
7  },
8  properties: function ({url, properties: p}, context) {
9    return {
10      label: p.pageTitle,
11      href: url
12    }
13  }
14}

There are two ways that the cache is invalidated:

1. implicit invalidation

2. explicit invalidation

This first example on referenced data, is the implicit invalidation. The reference works like a kind of donut caching, so that when the source entity that is referenced is updated, only the "hole" of the donut needs updating.

The explicit invalidation is used when you are working with dynamic lists based on the source entities, like all children of a source entity, as children can be added or removed without the parent is aware of it.

Updating lists

When you add or remove source entities that potentially could change the order of a list, you can use the actions method.

Below is an example of a navigation schema that dynamically references all its children.

1// navigation schema with references to all it's children
2
3/** @type {Enterspeed.FullSchema} */
4module default {
5  triggers: function(context) {
6    return context.triggers('cms', ['homePage'])
7  },
8  properties: function ({url, properties: p}, context) {
9    return {
10      children: context.reference('navigationItem').children()
11    }
12  }
13}

If children are added or removed the list in the navigation schema needs to be updated to reflect that. To do that we use the reprocess action defined in the navigationItem schema below. This will make sure to reprocess the navigation schema so that the list of children is updated.

1// navigationItem schema with an action to reprocess the homepage 
2
3/** @type {Enterspeed.FullSchema} */
4module default {
5  triggers: function(context) {
6    return context.triggers('cms', ['contentPage'])
7  },
8  action: function(context) {
9    return context.reprocess('homePage').parent()
10  },
11  properties: function ({url, properties: p}, context) {
12    return {
13      label: p.pageTitle,
14      href: url
15    }
16  }
17}

Ready to try out Enterspeed? 🚀

Start combining & connecting your services today

Product

Why Enterspeeed?Use casesBuild vs. buyIntegrations

Company

Partners ☕ Let's talk!About UsContact UsTerms of ServicePrivacy PolicySecurity
Enterspeed logo

© 2020 - 2024 Enterspeed A/S. All rights reserved.

Made with ❤️ and ☕ in Denmark.