Caching Best Practices for ASP.NET Core Microservices with NCache

Introduction

These days development teams are looking to redesign and re-architect their monolith applications into microservices for the variety of benefits it offers. Cloud-native architectures recommend microservice architecture and offer a variety of serverless solutions that make developing and maintaining microservices real quick.

What is Microservice Architecture?

In this architecture, we design applications as a collection of small, loosely coupled, and independent services. Each service in the stack is responsible for a single feature and encapsulates functionality for the same. This allows each service to be built, run, and scaled independently based on the requirement and load.

Microservice architecture creates opportunities for modular and individual development cycles without having to depend on other services in the application. We can build, deploy and maintain every microservice as required, as if each microservice is an application on its own. 

It offers improved scalability, resilience, and the ability to adopt different technologies for each service.

Importance of Caching Layer in Microservice Architecture

In Microservices, caching helps application performance by reducing repeated requests of frequently accessed data and therefore reducing the load on the backend. Frequently accessed data can be stored in the caching layer, and microservices can look up the cache for data and retrieve from it. This significantly improves response time and reduces unnecessary database hits.

Key Considerations for Caching in Microservices

While it is beneficial for microservice applications to add caching to boost application performance, it is equally important to ask and be clear about some questions.

  • Types of Caching - Where to Cache?
  • Caching Patterns - How to Cache?
  • Best Practices - When to Cache?

Where to Cache in a Microservice Architecture

Caching can be implemented in multiple ways and areas that suit the best.

For example, clients can maintain a local cache of their own to reduce unwanted requests to the server and improve application performance by storing frequently accessed assets such as configurations, images, etc.

On the server side, each microservice can maintain its own In-Process In-Memory cache within which it can store and access data it has previously obtained. 

This approach suits best monolithic applications since there is a single large application that is handling the requests.

But in a microservice architecture where services are deployed and run in their own individual containers, this approach may introduce duplicate data. Also, the services may maintain different versions of the data depending on the time they are cached.

Alternatively, caching can be externalized into a separate tier called Distributed Cache which is a cluster of cache nodes.

Each application microservice connects to this centralized distributed cache via a local cache client and accesses the cached objects. Each microservice can query and access the cache for the data it requires, which may be placed by some other service.

There are several popular solutions available in the market, such as Redis, Memcached, NCache, etc. For example, NCache is a popular distributed caching solution designed for .NET applications. It offers several features and functionalities that highly complement .NET microservice solutions.

How to Cache in a Microservice Architecture?

It is important to design how the application writes and reads from the cache. The following are some of the popular caching patterns that applications can choose to implement based on their business requirements and data volatility.

Lazy Loading/Cache-Aside

In this approach, a microservice loads the cache with data only when needed, and the microservice decides if the object needs to be cached.

For all subsequent requests, if the requested data is present in the cache, the service fetches from the cache and uses it. If the data is not present in the cache, the microservice queries data from the backend and then writes it to the cache before returning a response.

This pattern, also called as Cache-Aside pattern, is a widely used caching pattern. The advantage is that there is no backend query if the cache contains the requested object; otherwise, the service needs to query from the backend and then write to the cache. 

Read-Through/Write-Through

In this pattern, the cache is proactively loaded with data when the application starts. Any microservice reads through the cache before querying the backend. Since there is already data available, the microservice queries from the cache and returns. When there is any update in the data, the microservice first updates the cache and then writes the changes to the backend. 

The updates are synchronous between the cache and the backend. The cache is always warm with the latest data available in this approach. The only downside is that the data is maintained in the cache, whether needed or not.

Write-Behind

In this approach, the microservice writes its updates to the cache and returns without saving the changes to the backend. The cache manages the updates and writes to the backend in an asynchronous manner. When a client requests for data, the application services query the cache for data, and if not present, it fetches from the backend and writes to the cache.

NCache is one of those few caching providers which support Write-Behind caching features. You can learn more about it from their official documentation.

Best Practices for Caching in Microservices using NCache

Let's go through some of the best practices one needs to consider while designing Caching in Microservice architecture.

Cache Partitioning

Cache partitioning means dividing the cache space into smaller partitions, each dedicated to a service or data subset spread across nodes. This balances the read and write loads on the cache. It ensures that frequently accessed data remains cached and avoids unnecessary cache invalidations. In a distributed cache cluster, each node is called a Partition.

Cache Consistency

In a distributed caching cluster, multiple cache instances are spread across different nodes. Data is cached and replicated or partitioned across the nodes, and maintaining data consistently across the nodes is a bit tricky. We can use techniques such as Cache Invalidation or Cache coherence to maintain and synchronize data across the cluster.

Cache Invalidation

A Cache is a limited memory and can run out of space if data is not removed when not needed. This technique is called cache invalidation. When we invalidate a cache entry, it removes the entry from the cache and makes room for new entries to be cached. But deciding when to mark an entry to be removed is a decision to be taken. 

For starters, we can use a Time-based approach, where we maintain a TTL (Time To Live) while adding an entry to the cache, after which the entry is removed from the cache automatically. Deciding an ideal TTL that is not too long or too short is core to using this approach. It's up to us to decide based on the business requirements and behavior.

We can further improve by adding a Sliding Expiration, where the TTL is extended based on how frequently the data is being accessed from the cache.

When a microservice writes to the cache in a Write-Through approach, we can invalidate the existing cache entry and write a new entry.

In a microservice architecture, different microservices work in parallel with data. Whenever a microservice updates a data item, it can create an event that notifies other microservices to invalidate that item from the cache and remove it. We can use a Pub-Sub messaging system to implement this.

Conclusion

Caching is one of the most important techniques used for performance optimization. It significantly improves the performance, reliability, and scalability of Microservice applications by reducing the load on the backend. However, asking the right questions, such as where to cache, how to cache, and what to cache, is the key to designing efficient and effective caching.

Here are some best practices for caching we can implement in Microservices -

  1. Identify the right and eligible data that is best suited to be cached
  2. Decide on the right cache expiration policies and TTL based on the data behavior and requirements
  3. Implement cache invalidation to free up cache and keep data updated
  4. Implement systems that invalidate cache immediately as data changes in the backend
  5. Consider caching strategies such as cache-aside to lazyload data into cache only when requested
  6. Keep track of the cache hit rates and decide on what to cache and what not to
  7. Ensure that the size of the data being cache is appropriate, so that it doesn't fill up the cache fast
  8. Consider caching only parts of the data rather than the complete dataset unless required
  9. Employ distributed caching when there are multiple microservices running in an application
  10. Actively monitor and keep track of the cache performance and health