Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clean Architecture and Cache Invalidation

I have an app that tries to follow the Clean Architecture and I need to do some cache invalidation but I don't know in which layer this should be done.

For the sake of this example, let's say I have an OrderInteractor with 2 use cases : getOrderHistory() and sendOrder(Order).

The first use case is using an OrderHistoryRepository and the second one is using a OrderSenderRepository. Theses repositories are interfaces with multiple implementations (MockOrderHistoryRepository and InternetOrderHistoryRepository for the first one). The OrderInteractor only interact with theses repositories through the interfaces in order to hide the real implementation.

The Mock version is very dummy but the Internet version of the history repository is keeping some data in cache to perform better.

Now, I want to implement the following : when an order is sent successfully, I want to invalidate the cache of the history but I don't know where exactly I should perform the actual cache invalidation.

My first guess is to add a invalidateCache() to the OrderHistoryRepository and use this method at the end of the sendOrder() method inside the interactor. In the InternetOrderHistoryRepository, I will just have to implement the cache invalidation and I will be good. But I will be forced to actually implement the method inside the MockOrderHistoryRepository and it's exposing to the outside the fact that some cache management is performed by the repository. I think that the OrderInteractor should not be aware of this cache management because it is implementation details of the Internet version of the OrderHistoryRepository.

My second guess would be perform the cache invalidation inside the InternetOrderSenderRepository when it knows that the order was sent successfully but it will force this repository to know the InternetOrderHistoryRepository in order to get the cache key used by this repo for the cache management. And I don't want my OrderSenderRepository to have a dependency with the OrderHistoryRepository.

Finally, my third guess is to have some sort of CacheInvalidator (whatever the name) interface with a Dummy implementation used when the repository is mocked and an Real implementation when the Interactor is using the Internet repositories. This CacheInvalidator would be injected to the Interactor and the selected implementation would be provided by a Factory that's building the repository and the CacheInvalidator. This means that I will have a MockedOrderHistoryRepositoryFactory - that's building the MockedOrderHistoryRepository and the DummyCacheInvalidator - and a InternetOrderHistoryRepositoryFactory - that's building the InternetOrderHistoryRepository and the RealCacheInvalidator. But here again, I don't know if this CacheInvalidator should be used by the Interactor at the end of sendOrder() or directly by the InternetOrderSenderRepository (even though I think the latter is better because again the interactor should probably not know that there is some cache management under the hood).

What would be your preferred way of architecturing this ?

Thank you very much. Pierre

like image 398
pdegand59 Avatar asked Aug 31 '16 09:08

pdegand59


People also ask

What is meant by cache invalidation?

Cache invalidation refers to process during which web cache proxies declare cached content as invalid, meaning it will not longer be served as the most current piece of content when it is requested. Several invalidation methods are possible, including purging, refreshing and banning.

When should cache be invalidated?

Cache invalidation is a process where the computer system declares the cache entries as invalid and removes or replaces them. The basic objective of using cache invalidation is that when the client requests the affected content, the latest version is returned.

Why is cache invalidation important?

Cache invalidation describes the process of actively invalidating stale cache entries when data in the source of truth mutates. If a cache invalidation gets mishandled, it can indefinitely leave inconsistent values in the cache that are different from what's in the source of truth.


1 Answers

Your 2nd guess is correct because caching is a detail of the persistence mechanism. E.g. if the repository would be a file based repository caching might not be an issue (e.g. a local ssd).

The interactor (use case) should not know about caching at all. This will make it easier to test because you don't need a real cache or mock for testing.

My second guess would be perform the cache invalidation inside the InternetOrderSenderRepository when it knows that the order was sent successfully but it will force this repository to know the InternetOrderHistoryRepository in order to get the cache key used by this repo for the cache management.

It seems that your cache key is a composite of multiple order properties and therefore you need to encapsulate the cache key creation logic somewhere for reuse.

In this case, you have the following options:

One implementation for both interfaces

You can create a class that implements the InternetOrderSenderRepository as well as the InternetOrderHistoryRepository interface. In this case, you can extract the cache key generation logic into a private method and reuse it.

Use a utility class for the cache key creation

Simple extract the cache key creation logic in a utility class and use it in both repositories.

Create a cache key class

A cache key is just an arbitrary object because a cache must only check if a key exists and this means use the equals method that every object has. But to be more type-safe most caches use a generic type for the key so that you can define one.

Thus you can put the cache key logic and validation in an own class. This has the advantage that you can easily test that logic.

public class OrderCacheKey {

    private Integer orderId;
    private int version;

    public OrderCacheKey(Integer orderId, int version) {
        this.orderId = Objects.requireNonNull(orderId);
        if (version < 0) {
            throw new IllegalArgumentException("version must be a positive integer");
        }
        this.version = version;
    }

    public OrderCacheKey(Order order) {
        this(order.getId(), order.getVersion());
    }

    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        OrderCacheKey other = (OrderCacheKey) obj;

        if (!Objects.equals(orderId, other.orderId))
            return false;

        return Objects.equals(version, other.version);
    }

    public int hashCode() {
        int result = 1;
        result = 31 * result + Objects.hashCode(orderId);
        result = 31 * result + Objects.hashCode(version);
        return result;
    }

}

You can use this class as the key type of your cache: Cache<OrderCacheKey, Order>. Then you can use the OrderCacheKey class in both repository implementations.

Introduce a order cache interface to hide caching details

You can apply the interface segregation principle and hide the complete caching details behind a simple interface. This will make your unit tests more easy because you have to mock less.

public interface OrderCache {

    public void add(Order order);
    
    public Order get(Integer orderId, int version);

    public void remove(Order order);
    
    public void removeByKey(Integer orderId, int version);
}

You can then use the OrderCache in both repository implementations and you can also combine the interface segregation with the cache key class above.

How to apply

  • You can use aspect-oriented programming and one of the options above to implement the caching
  • You can create a wrapper (or delegate) for each repository that applies caching and delegates to the real repositories when needed. This is very similar to the aspect-oriented way. You just implement the aspect manually.
like image 60
René Link Avatar answered Oct 01 '22 20:10

René Link