Skip to content

Improving Repeated Access with Cache-Aside Pattern

As a responsible developer, in addition to being able to write clean code, it is also important to ensure that our application runs efficiently. As we know, we must return results as quickly as possible to provide a better experience to customers. Well, one of the most basic performance improvements we can implement quickly is a cache implementation for repeated access.

Although it seems like a simple process, it gives our applications a significant performance increase in many aspects. In addition, it enables us to use our resources more effectively.

In this article, I will try to show how we can easily implement the Cache-Aside pattern in .NET.

Well,

As we can see from the diagram above, our approach is to always read data from the cache first.

If the data we want to get is not in cache, we need to get the data from the relevant data store and then return the response by adding it to the cache. Thus, by minimizing repeated access, we can provide better experiences to customers and use our resources much more effectively.

Consistency

The cache-aside pattern doesn’t provide any consistency guarantee. In other words, data we want to get may have been changed by a different process.

If we are working in a distributed environment, we can try to keep the data up-to-date as possible with pub/sub approach. Of course, in this case, we must also accept eventual consistency.

Expiration Policy

Another important issue is that the data we want to cache has an expiration policy. Otherwise, the data, which will be in the cache, may become invalid after a while.

We need to set the expiration policy carefully to provide consistency. If we set this expiration time too short, we will not get much benefit from this approach since the repeated access will occur again.

In short, we need to adjust this time carefully according to the domain we have.

Implementation

I will create a simple ASP.NET 5 Web API project for implementation. Our goal here is to be able to create a flexible and reusable cache structure with minimum effort while implementing the cache-aside pattern.

For this, let’s first create an interface called “ICacheService” as follows.

using System;
using System.Threading.Tasks;

namespace MyTodoAPI.Services
{
    public interface ICacheService
    {
        Task<T> GetOrSetAsync<T>(string cacheKey, int cacheDurationInMin, Func<Task<T>> func);
        Task Remove(string cacheKey);
    }
}

We will implement the cache-aside pattern in the “GetOrSetAsync” method. Also in the “Remove” method, we will add the required logic to invalidate the cache of any data we want.

As the cache service, we will implement Redis. For this, I will use Azure Redis Cache service. You can follow the steps here to have Azure Redis Cache service.

Before starting to the implementation, let’s add the “Microsoft.Extensions.Caching.StackExchangeRedis” package to the project via NuGet as follows.

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --version 5.0.1

Then, let’s implement the “ICacheService” interface by creating a class called “RedisCacheService“.

using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;

namespace MyTodoAPI.Services
{
    public class RedisCacheService : ICacheService
    {
        private readonly IDistributedCache _distributedCache;

        public RedisCacheService(IDistributedCache distributedCache)
        {
            _distributedCache = distributedCache;
        }

        public async Task<T> GetOrSetAsync<T>(string cacheKey, int cacheDurationInMin, Func<Task<T>> func)
        {
            string cachedItem = await _distributedCache.GetStringAsync(cacheKey);

            if(!string.IsNullOrEmpty(cachedItem))
            {
                return JsonSerializer.Deserialize<T>(cachedItem);
            }

            var item = await func();

            if (item != null)
            {
                var cacheEntryOptions = new DistributedCacheEntryOptions()
                                        .SetSlidingExpiration(TimeSpan.FromMinutes(cacheDurationInMin));

                string serializedItem = JsonSerializer.Serialize(item);

                await _distributedCache.SetStringAsync(cacheKey, serializedItem, cacheEntryOptions);
            }

            return item;
        }

        public async Task Remove(string cacheKey)
        {
            await _distributedCache.RemoveAsync(cacheKey);
        }
    }
}

If we look at the code above, we can see that we use the “IDistributedCache” interface that comes as build-in for caching operations. We will also inject the redis as an adapter in the next step.

In the “GetOrSetAsync” method, we have used the func delegate to get a reusable structure. We can also see that the method flow is similar to the diagram I have shared above.

In addition, we have implemented the “Remove” method to invalidate a cache.

Let’s make an example

To perform an example, I will go with “WeatherForecast” which comes with the default template.

For this, let’s create an interface called “IWeatherForecastService” and a class called “WeatherForecastService” as follows.

using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyTodoAPI.Services
{
    public interface IWeatherForecastService
    {
        Task<List<WeatherForecast>> GetWeatherForecasts();
        Task AddNewWeatherSummary(string summary);
    }
}
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyTodoAPI.Services
{
    public class WeatherForecastService : IWeatherForecastService
    {
        private readonly ICacheService _cacheService;

        private static List<string> Summaries = new()
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private const string GetWeatherForecastsCacheKey = "GetWeatherForecasts";
        private const string GetWeatherForecastsCacheDurationInMin = 30;

        public WeatherForecastService(ICacheService cacheService)
        {
            _cacheService = cacheService;
        }

        public async Task<List<WeatherForecast>> GetWeatherForecasts()
        {
            List<WeatherForecast> weatherForecasts = await _cacheService.GetOrSetAsync(cacheKey: GetWeatherForecastsCacheKey,
                                                                                       cacheDurationInMin: GetWeatherForecastsCacheDurationInMin,
                                                                                       func: () =>
            {
                var rng = new Random();
                var weatherForecasts = new List<WeatherForecast>();

                foreach (var item in Summaries)
                {
                    weatherForecasts.Add(new WeatherForecast
                    {
                        Date = DateTime.Now,
                        TemperatureC = rng.Next(-20, 55),
                        Summary = item
                    });
                }

                return Task.FromResult(weatherForecasts);
            });

            return weatherForecasts;
        }

        public Task AddNewWeatherSummary(string summary)
        {
            Summaries.Add(summary);

            _cacheService.Remove(GetWeatherForecastsCacheKey);

            return Task.CompletedTask;
        }
    }
}

We have implemented a simple logic using the “ICacheService” in the “GetWeatherForecasts” method. If the data we want has been added to the cache before, we will be able to get this data quickly through the cache.

Also, if we want to add a new “summary”, we will invalidate the corresponding cache with the “Remove” method.

At last let’s perform the injection operations in the “Startup” as follows.

services.AddStackExchangeRedisCache(opt =>
{
    opt.Configuration = Configuration.GetConnectionString("redis:connection_string");
});

services.AddSingleton<ICacheService, RedisCacheService>();
services.AddScoped<IWeatherForecastService, WeatherForecastService>();

Conclusion

As we can see, the cache-aside pattern is very easy to implement. It allows us to add the desired data to the cache at the demand of the applications and get the cached data. Thus, we can benefit from caching operations as much as possible even in case of unforeseen situations.

References

Cache-Aside pattern – Cloud Design Patterns | Microsoft Docs

Published in.NET CoreArchitecturalAzuredotnetTasarım Kalıpları (Design Patterns)

2 Comments

  1. I developed to library for cache operations.
    This library includes MemoryCache, Redis and Memcache. You can be Eaisly change cache provider.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.