How to implement Write-Through Cache in ASP.NET Core

Introduction

A Cache is a high-speed memory that is used to store and access frequently used data by the application. It is one of the several techniques used to optimize application performance. It reduces the overall execution time and the cost to query a backend for data.

Different applications use different caching patterns to store and access data from a cache. It depends on the application usage, business requirements and the tolerance for stale data.

Based on these factors, there are 3 major patterns for working with a cache. They are –

  1. Cache Aside pattern
  2. Read Through / Write Through pattern
  3. Write Behind pattern

What is a Write-Through Cache pattern

In the Read-Through/Write-Through cache pattern, the cache is seeded with data when the application starts.

It is done no matter whether the data will be requested or not.

The data is already available in cache as the application starts and any service reads through the cache before querying the backend.

Credits: Write-Through caching patterns, AWS

Since there is already data available, the service queries from the cache and returns a response without having to fetch from the backend.

When there is any update in the data, the service first updates the cache and then writes the changes to the backend. The updates are synchronously maintained between the cache and the backend.

Since the writes are committed in the backend via the cache, this approach is called Write-Through caching.

When to use Write-Through Cache pattern

Sometimes we develop applications that access huge sets of data that are updated very less frequently.

For example, applications that query a Data Warehouse or a Mainframe system where data is updated via batch processes and every read may consume a lot of resources to compute and process.

In such cases, we can opt for a Read-Through/Write-Through approach where the cache is seeded with precomputed data and is queried for results.

Since the data is already known to be stale and less frequently updated, we don’t need to worry about it.

Benefits of Write-Through Cache pattern

  • Always Warm – Since cache is seeded with data, it always contains data and the need for a backend query is minimalized.
  • Less Stress on the Backend – Since almost all the objects are available in the cache, the load on the database for queries is reduced and its performance is at optimal levels.

Key Considerations while using Write-Through Cache pattern

Seeding the cache with data and writing updates to cache immediately after a database update is core to Write-Through cache.

  • Data is maintained almost always in the cache – Ideally a cache needs to maintain only the data that is frequently accessed. Since we are populating cache with data beforehand, there are chances that we are maintaining data that is not needed to be cached.
  • Write Penalty – Since cache always has the data available, there is no read penalty (cache miss). However, there is always a write penalty since data is written at the cache and at the database.
  • Cache size – since we are storing objects in cache, we need to be aware of the size constraints of the cache and the objects being stored. Cache is a high-speed memory that can have costs associated with it.

Cache-Aside reads with Write-Through writes

Generally, we combine cache aside pattern for reads with write-through for writes in normal applications where the reads are not so costly. This gives us the best of both worlds – a cache that has the latest data available for querying.

Implementing Write-Through Cache in ASP.NET Core with an Example

As mentioned above, a service updates a cache almost immediately after a data is updated in the backend database. The pseudo code for a simple update function will be as follows –

update_entity(entity_id, entity)
    entity = db.execute("UPDATE Entities SET price = {0} WHERE id = {1}", entity.price, entity_id)
    cache.set(entity_id, entity)

    return success

In ASP.NET Core we can implement a write-through cache as below

To demonstrate, let us assume we have an EntityService class that wraps the Reads and Writes on an entity with two methods.

  • GetEntityById method returns a single Entity that matches the entityId passed.
  • UpdateEntity updates the entity in the backend with a new object and returns the updated entity.

We implement a Write-Through cache while writing updates to the database in the UpdateEntity method.

We can also implement the Cache-Aside pattern inside the GetEntityById method that lazy loads the entity to the cache on a miss.

Entity Service that encapsulates Reads and Writes

The EntityService implementation is as below. It does both write-through update and cache-aside for reads.

public interface IEntityService
{
    Entity GetEntityById(int entityId);
    Entity UpdateEntity(int entityId, Entity entity);
}

public class EntityService : IEntityService
{
    private const int TIME_TO_LIVE_IN_SECONDS = 600;

    private readonly IEntityRepository _entityRepository;
    private readonly ICachingService<Entity> _cachingService;

    public EntityService(IEntityRepository entityRepository, ICachingService<Entity> cache)
    {
        _entityRepository = entityRepository;
        _cachingService = cache;
    }

    public Entity GetEntityById(int entityId)
    {
        if (!_cachingService.TryGetCachedEntity(entityId, out Entity Entity))
        {
            var record = _entityRepository.GetEntityById(entityId);

            if (record != null)
            {
                return _cachingService.AddEntityToCache(record.Id, record, TIME_TO_LIVE_IN_SECONDS);
            }
        }

        return Entity;
    }

    public Entity UpdateEntity(int entityId, Entity entity)
    {
        var updatedEntity = _entityRepository.UpdateEntity(entityId, entity);
        var cachedEntity = _cachingService.AddEntityToCache(entityId, updatedEntity, TIME_TO_LIVE_IN_SECONDS);

        return cachedEntity;
    }
}
Caching Services that writes to Cache

The CachingService encapsulates a generic caching functionality to add to cache and retrieve from cache. It is implemented as below.

For example, a CachingService class implementation that uses InMemory caching is as below –

public interface ICachingService<TClass> where TClass : class
{
    TClass AddEntityToCache(object cacheKey, TClass entity, int timeToLive);
    bool TryGetCachedEntity(object cacheKey, out TClass cachedEntity);
}

public class CachingService<TClass> : ICachingService<TClass> where TClass : class
{
    private readonly IMemoryCache _inMemoryCache;

    public CachingService(IMemoryCache cache)
    {
        _inMemoryCache = cache;
    }

    public bool TryGetCachedEntity(object cacheKey, out TClass cachedEntity)
    {
        if (!_inMemoryCache.TryGetValue(cacheKey, out cachedEntity))
        {
            return false;
        }

        return true;
    }

    public TClass AddEntityToCache(object cacheKey, TClass entity, int timeToLive)
    {
        var cacheOptions = new MemoryCacheEntryOptions();
        cacheOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(timeToLive);

        _inMemoryCache.Set(cacheKey, entity, cacheOptions);
        return entity;
    }
}

Buy Me A Coffee
Enjoyed my content and found it useful?
Consider buying me a coffee to support my work and keep the content flowing!

Conclusion

Write-Through cache is one of the popular caching strategies that determines how to write an object to the cache. In this approach, all the writes to the database are immediately written to the cache as well, which reduces the case for a cache miss.

Cache is warmed with data before the application starts and almost all the reads are done through the cache without having to hit the backend. This approach is called a read-through.

This approach is well suited for systems where the reads are heavy and the writes are very rare or periodic. One key consideration – we may maintain data in the cache that may not be frequently read, which associates to the overall cost and performance.

In common practice, we can use a cache-aside pattern for our reads and a write-through for updates, which helps us keep the latest data available in the cache at all times.

Do you think a write-through pattern is the best fit for your application? Let me know in the comments below. 👇

Sriram Mannava
Sriram Mannava

A software Engineer from Hyderabad, India with close to 9 years experience as a full stack developer in Angular and .NET space.

Articles: 5

One comment

Comments are closed.