Cache Strategies

Moshe Binieli
14 min readApr 20, 2023

--

Introduction

Caching is a technique used to improve the performance of web applications. In simple terms, caching involves storing frequently accessed data or content in a temporary storage location called a cache. This enables web applications to serve content to users more quickly, without having to fetch it from the original source every time it is requested.

One of the primary benefits of caching is improved application performance. By reducing the time it takes to access frequently requested content, web applications can respond to user requests more quickly, improving user experience and overall application performance. Additionally, caching can help reduce the load on web servers and other resources, which can help reduce costs and improve scalability.

Caching is suitable for frequently accessed data without negatively affecting data integrity or application functionality. Common use cases include serving static assets like images, stylesheets, and scripts, caching database queries or API responses, and caching pages or content for dynamic web apps. However, consider the caching strategy for each app and regularly refresh or invalidate cached content to avoid serving stale content.

Introduction image, created by DALL-E

Table of Contents

  1. Cache Types (In-memory caching, Distributed caching, Client-side caching)
  2. Cache Strategies (Cache-Aside, Write-Through, Write-Behind, Read-Through)
  3. Measuring Cache Effectiveness (Calculate the cache hit rate, Analyze cache eviction rate, Monitor data consistency, Determine the right cache expiration time)
  4. Real Life examples (E-commerce website, Mobile banking app)
  5. Code examples for the 3 cache strategies
  6. Conclusions

Cache Types

In-memory caching

In-memory caching is a type of caching that involves storing data in the computer’s RAM (Random Access Memory) instead of in a database or on disk. This type of caching is useful for applications that require high-speed access to data, such as web servers and databases. In-memory caching can significantly improve the performance of an application by reducing the number of database queries and disk reads required to retrieve data. However, it is important to note that in-memory caching is volatile and the data stored in the RAM may be lost if the system is shut down or restarted.

Example: Imagine you have a web application that frequently needs to retrieve a list of products from a database. You could use in-memory caching to store the list of products in memory after the first retrieval, so that subsequent requests for the same data can be served directly from memory instead of hitting the database each time.

Distributed caching

Distributed caching is a type of caching that involves storing data across multiple servers or nodes in a network. This type of caching is useful for applications that require high availability and scalability. Distributed caching allows multiple servers to share the workload of storing and retrieving data, which can improve the performance of the application and reduce the risk of data loss. However, managing a distributed caching system can be complex, and ensuring consistency across multiple nodes can be challenging.

Client request checks cache for data, if available returns it, if not, retrieves data from database, stores it on cache and returns to client

Example: Let’s say you have a large-scale e-commerce website that serves customers all over the world. To ensure that product information is readily available to customers no matter where they are located, you could use a distributed caching solution such as Redis or Memcached to store product data in memory across multiple servers in different regions. This would help reduce latency and improve overall site performance.

Client-side caching

Client-side caching is a type of caching that involves storing data on the client’s device, such as a web browser. This type of caching is useful for web applications that require frequent access to static resources, such as images and JavaScript files. Client-side caching can significantly improve the performance of a web application by reducing the number of requests made to the server. However, it is important to note that client-side caching can lead to issues with stale data, as the cached data may not always be up-to-date. Therefore, careful consideration should be given to the caching policies and expiration times used in client-side caching.

Example: Imagine you have a web application that frequently displays images or other static content that doesn’t change very often. You could use client-side caching to store the images in the user’s browser cache after the first retrieval, so that subsequent requests for the same content can be served directly from the cache instead of having to download the content again from the server. This can help improve page load times and reduce network traffic.

Cache Strategies

Cache-Aside

In this strategy, the application is responsible for managing the cache. When data is requested, the application checks the cache first. If the data is not in the cache, it is retrieved from the database and stored in the cache for future use. This strategy is simple and flexible, but it requires careful management of the cache to ensure that it remains up-to-date.

Write-Through

In this strategy, data is written to both the cache and the database at the same time. When data is updated, it is written to the cache and the database simultaneously. This ensures that the cache always contains up-to-date data, but it can slow down write operations.

Write-Behind

In this strategy, data is written to the cache first and then to the database at a later time. This allows write operations to be faster, but it can lead to data inconsistencies if the cache is not properly managed.

Read-Through

In this strategy, the cache is used as the primary data source. When data is requested, the cache is checked first. If the data is not in the cache, it is retrieved from the database and stored in the cache for future use. This strategy can be useful when the database is slow or when data is frequently read but rarely updated.

Measuring Cache Effectiveness

Here are some steps to measure the effectiveness of your caching strategy and choose the right cache expiration time:

Calculate the cache hit rate

The cache hit rate is the percentage of requests that are served from the cache instead of the backend data store. A high hit rate indicates that your caching strategy is effective in reducing the load on the backend data store. You can measure the hit rate by dividing the number of requests served from the cache by the total number of requests.

Analyze cache eviction rate

The cache eviction rate is the percentage of cached items that are removed from the cache due to expiration or replacement. A high eviction rate can indicate that the cache expiration time is too short or the cache size is too small.

Monitor data consistency

Data consistency is critical in caching. If the cached data becomes stale or outdated, it can lead to incorrect results and compromise the integrity of your application. You can monitor data consistency by comparing the cached data with the data in the backend data store.

Determine the right cache expiration time

The cache expiration time determines how long the cached data remains valid before it is evicted from the cache. A longer cache expiration time can improve the cache hit rate but increase the risk of stale data. A shorter cache expiration time can reduce the risk of stale data but decrease the cache hit rate. You can choose the right cache expiration time based on the data volatility and the acceptable level of data staleness.

Real Life examples (2 Examples)

Example 1: E-commerce website

Imagine an e-commerce website that sells various products online. One of the most popular products is a pair of shoes. This website receives a large number of requests every second, and as a result, it can slow down the response time for customers. To improve the performance of the website, the developers decide to implement caching.

In-memory caching: The developers use in-memory caching to store frequently accessed product information, such as the price, description, and image of the shoes. This information is stored in the RAM of the web server, which allows for fast access and retrieval. When a user requests the product page, the server checks the cache first before querying the database. If the product information is in the cache, the server can retrieve it quickly without hitting the database.

Distributed caching: As the website continues to grow, the developers realize that a single web server is not enough to handle the increasing traffic. They decide to use a distributed caching system to improve the scalability and availability of the website. They choose a distributed cache that can be accessed by multiple web servers, and configure it to automatically synchronize the data between nodes.

Client-side caching: To further improve the user experience, the developers also implement client-side caching. They use browser caching to store static resources, such as CSS, JavaScript, and images, on the user’s computer. This allows the website to load faster for returning users, as the browser can retrieve the resources from the cache instead of downloading them again.

Example 2: Mobile banking app

Consider a mobile banking app that allows users to check their account balance, transfer funds, and pay bills. This app needs to access customer account information quickly and securely. To achieve this, the developers decide to use caching.

In-memory caching: The developers use in-memory caching to store frequently accessed account information, such as the account balance and transaction history. This information is stored in the RAM of the mobile device, which allows for fast access and retrieval. When a user logs in to the app, the app checks the cache first before querying the server. If the account information is in the cache, the app can retrieve it quickly without sending a request to the server.

Distributed caching: The app needs to handle a large number of concurrent users, so the developers decide to use a distributed caching system to improve the scalability and availability of the app. They choose a distributed cache that can be accessed by multiple app servers, and configure it to automatically synchronize the data between nodes.

Client-side caching: To further improve the user experience, the developers also implement client-side caching. They use local storage to store user preferences, such as the default account and payment information. This allows the app to load faster for returning users, as the app can retrieve the preferences from the cache instead of asking the user to input them again.

Code examples for the 3 cache strategies

Check out the 3 cache strategies I created and uploaded on GitHub. Clone the project and start experimenting with it. Also, continue reading the article to learn how each strategy is implemented and how it works.

GitHub: MosheWorld/Cache-Strategies (github.com)

In-memory caching

GitHub: Cache-Strategies/In Memory Cache at main · MosheWorld/Cache-Strategies (github.com)

The cache.js file contains functions that handle operations against the cache. To use this cache, a class is defined with a Map property, along with class functions to manage the cache such as put, get, delete, clear, keys, and size.

The put function adds a key-value pair to the cache and allows for a specified expiration time. If no expiration time is provided, the default expiration time for the cache is used.

put(key, value, expirationTime = this.expirationTime) {
const now = new Date().getTime();
const expiration = now + expirationTime;

this.cache.set(key, { value, expiration });
}

On the other hand, the get function verifies whether the key exists and whether it has expired. If the key has not expired, it returns the corresponding value. If it has expired or does not exist, it returns null.

get(key) {
const now = new Date().getTime();
const cached = this.cache.get(key);

if (cached && cached.expiration > now) {
return cached.value;
}

this.cache.delete(key);
return null;
}

On the main.js method you can see that the code uses a cache to temporarily store three objects. Two objects have keys ‘myKey1’ and ‘myKey2’ with values of ‘hello’ and ‘world’, while the third object has a key ‘myKey3’ with a value of ‘foo’ and an expiration time of 5 seconds. The objects are retrieved and printed to the console. After 6 seconds, an attempt to retrieve the expired object is made, but it returns null.

const { Cache } = require('./cache.js');

const myCache = new Cache(); // expires after 30 minutes by default

// Add objects to the cache with default expiration time
myCache.put('myKey1', { myValue: 'hello' });
myCache.put('myKey2', { myValue: 'world' });

// Add an object to the cache with a custom expiration time
myCache.put('myKey3', { myValue: 'foo' }, 5 * 1000); // expires after 5 seconds

// Retrieve objects from the cache
const myObject1 = myCache.get('myKey1');
const myObject2 = myCache.get('myKey2');
const myObject3 = myCache.get('myKey3');

console.log(myObject1); // { myValue: 'hello' }
console.log(myObject2); // { myValue: 'world' }
console.log(myObject3); // { myValue: 'foo' }

// Wait for 6 seconds (longer than the expiration time of myKey3)
setTimeout(() => {
const myObject3Expired = myCache.get('myKey3');
console.log(myObject3Expired); // null
}, 6000);

Client-side caching

GitHub: Cache-Strategies/Client Side Cache at main · MosheWorld/Cache-Strategies (github.com)

The crypto-prices.html file contains a HTML page that displays the current price of Bitcoin and Ethereum in US dollars. The page has two buttons, “Get Bitcoin Price” and “Get Ethereum Price”.

When a user clicks on one of the buttons, the JavaScript code fetches the current price of the selected cryptocurrency from some API and displays it on the page.

The JavaScript code also checks if the price data is already stored in the local storage of the user’s browser. If the data is found in the local storage and is less than 10 seconds old, the JavaScript code retrieves the price from the local storage and displays it on the page. Otherwise, the JavaScript code fetches the current price from the API, stores it in the local storage, and displays it on the page.

Moreover, by accessing the developer tools and selecting Application, followed by Local Storage, one can observe that BTC and ETH are saved in the local storage along with a timestamp indicating the elapsed time of 10 seconds.

In the code, there is usage of the localstorage variable. For example:

1. const cachedData = localStorage.getItem(currency);
2. localStorage.setItem(currency, cachedData);

You may find it helpful to read about it here for further understanding: Window: localStorage property — Web APIs | MDN (mozilla.org)

Distributed caching

GitHub: Cache-Strategies/Distributed Cache at main · MosheWorld/Cache-Strategies (github.com)

In the Distributed Cache folder, there are multiple files. To begin with, after cloning the project, execute the command `npm install` to install the necessary packages for the Distributed Cache illustration.

After installing the required packages, we are going to use a Redis. Redis is a fast and flexible in-memory data store that supports various data structures and features such as caching, messaging, and high availability.

Go to: Redis | The Real-time Data Platform, and click on “Try Free” and create a new account. After accessing the main screen, navigate to the databases section and proceed to open the Redis database that you have recently created.

Then you’ll need to copy the necessary credentials and then we can start using Redis from the code. Go to:

Select Redis Client and copy the host, and port.

Then scroll down to “Security” and copy the password.

Please return to the GitHub repository that you cloned and navigate to the Distributed Cache folder. Within this folder, you should locate the config.json file. Take the 3 arguments that you just copied and place them in their corresponding positions within the file.

{
"host": "<Replace with the host>",
"port": "Replace with the port",
"password": "<Replace with the password>"
}

We can begin executing the code now and observe its functionality. Our task involves running two files, namely redis-writer.js and redis-reader.js. The former establishes a connection to the Redis server we have recently registered and assigns a new key-value pair to it. The latter creates a connection to the same Redis server and retrieves the key’s value from the database, displaying it on the console.

To establish a connection to the Redis server, we utilize the “ioredis” package installed earlier via “npm install”. We create an instance of “ioredis” and provide the appropriate arguments using the values filled in the config.json file.

const Redis = require('ioredis');
const config = require('./config.json');

const redis = new Redis({
host: config.host,
port: config.port,
password: config.password
});

As you can see the set value to Redis (Written in redis-write) is pretty simple.

async function setRedisKey() {
try {
await redis.set(MY_KEY, 'Hello, Redis!', 'EX', EXPIRATION_TIME); // set the key with the expiration time
console.log('Finished settings the key to Redis successfully');
} catch (error) {
console.error('Failed to set key:', error);
}
}

And the read value from Redis (Written in redis-reader) is pretty simple.

async function getRedisKey() {
try {
const value = await redis.get(MY_KEY);
console.log('Retrieved value:', value);
} catch (error) {
console.error('Failed to get key:', error);
}
}

Next, we need to open two separate terminals. In one terminal, we should navigate to redis-writer, while in the other, we should navigate to redis-reader.

Execute the redis-writer.js script, and upon successful completion, you will be greeted with the message: “Finished settings the key to Redis successfully”. This indicates that the key-value pair has been successfully stored in the Redis database.

Immediately within 10 seconds, run the redis-reader.js script and observe how it retrieves the key from the Redis cache server and prints it out.

If you run the redis-reader.js script again after 10 seconds, you’ll notice that the key has expired and the Redis server will return NULL.

Conclusions

In conclusion, cache strategies are crucial for optimizing application performance and improving the user experience. Utilizing different cache types and strategies, measuring cache effectiveness, and implementing the appropriate approach can significantly reduce page load times, improve search performance, and enhance data processing capabilities. Understanding and implementing cache strategies is a critical component of application development.

Every comment is welcome, so if you come across any mistakes in the article, don’t hesitate to contact me through LinkedIn — Click Here

--

--