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
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.
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.
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.
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 theInternetOrderHistoryRepository
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With