For one of my microservices projects, I decided to structure the code as controllers, services, and repositories. The API call routes to the controller, which then calls the service, which then calls the repository. Each of these layers interacts with the next layer via an interface to keep things decoupled.
All was fine until I needed a cache.
Imagine I already have a CustomerRepository interface having findById(int id) method, with one implementation that interacts with DB (DbCustomerRepositoryImpl). Now I need to cache this information due to some reason. So I create an implementation of CustomerRepository that interacts with the cache (CacheCustomerRepositoryImpl), findById(int id) does a simple cache.get() and returns the response. But where do I handle the cache miss?
- I could handle the cache miss in the service layer, and interact with the
DbCustomerRepositoryImplof CustomerRepository when there is a cache miss, (and potentially write in the cache). But this leads to the coupling of the service and the repository. If down the line, I decide to only useDbCustomerRepositoryImpl, then I would have unwanted code that handles cache miss in service, or in the worst case, need some code changes in the service. - I could handle the cache miss in the repository layer - the
CacheCustomerRepositoryImplwill first check in the cache, in the case of a cache miss, interact with theDbCustomerRepositoryImpland return the result to the service. This looks fine because according to me, the repository layer should just get the data, no matter from where, it doesn't matter to the service layer. But this has a pitfall. If I need to cache the business logic's response, what should I do? Would the service layer callCacheCustomerRepositoryImpland then this call the service layer? This kind of adds circular dependency in the application which I am trying to avoid.
I could think of these 2 cases, each case might have its own pros and cons. But I need some help to figure out which might be the better case, or if there is a better approach to caching in clean architecture.
For most applications caching is a technical detail which, in Clean Architecture, should be in the interface adapters layer.
In order to cache data coming from a repository i would apply the decorator pattern. Your CachingRepository implements same interface as the actual repository, takes the cache implementation and the real repository as constructor dependency and then always first checks the cache on read and falls back on the real repository in case of cache miss. It updates the cache on write API calls. See also: https://youtu.be/e_-bf93vx10?si=LxN8jbHKmhu5PgRe
If you additionally need caching of the business logic response, I would add separate caching on controller level. In Asp.Net this can easily be done with a custom middleware.