Accelerate Microservices with Refresh-Ahead Caching

Refresh-Ahead Caching provides clients closely up-to-date data while benefitting from performance gain.

When designing a microservice architecture, we can encounter different performance issues. Reactive frameworks, like Akka, provide a way to make a microservice more resilient. However, when dealing with time-consuming algorithms or slow dependent systems, caching can be our last resort, although it will come with a trade-off. Data is usually outdated but it provides a performance gain.

A better solution to this problem is Refresh-Ahead Caching. While providing a performance gain, it also provides closely up-to-date data. The cache is being asynchronously reloaded by the microservice, while the client only accesses the fast cache resource. You can enable Refresh-Ahead Caching in your Spring Boot project with cache-refresh-ahead-spring-boot-starter. It runs a scheduler that refreshes the cache and supports Caffeine as well as Redis. You can specify the refresh interval either for all caches or only for specific caches.

In order to minimise the size of cache keys, it uses Class-names, Method-names, and Parameter-Classes. It resolves these attributes and unwraps the CGLIB proxy Bean. Usually, when you add a @Cacheable annotation to a method, the class is wrapped in a CGLIB proxy Bean. However, this proxy is only visible from other classes. So you cannot enable caching for private methods. This unwrapping allows the scheduler to invoke the method on the actual Bean without possibly hitting the cache. Values of a cache are updated on a lower level so they do not overwrite the write-time.

Spring Boot Refresh-Ahead Caching

When using a cache that sits in your microservice and scaling horizontally, you should be aware that your caches scatter. You could encounter the problem that cached data is too distributed and clients have a low probability to hit refreshed cache sources. The other problem you can encounter is that a high amount of microservices refresh their cache and exhaust the backend. Also, cache consistency can be an issue if you use the @CachPut annotation. Therefore, I recommend using a local cache only if you have a low amount of redundant microservices.

Microservices that are highly redundant should use a shared Redis cache. The refresh logic should also be separated from the executing microservices. Using this method, you could have, for instance, 10 executing microservices that access the cache and 3 (for redundancy) microservices that only refresh the cache, so you do not have to worry about exhausting your backend or scattered caches.

Next time you encounter an issue with a slow dependent system, you may want to consider Refresh-Ahead Caching as a solution.