A caching example from my Basket service
While building the Basket service in my microservices project I started with a simple goal. Persist and retrieve shopping cart data cleanly. At that stage only one repository existed and it worked well.
Later when performance needs became clear I introduced caching. What mattered to me was adding this behavior without rewriting existing code or leaking caching logic into handlers. This is where Repository and Decorator patterns quietly worked together.
Step 1 Start with a simple repository
In this step we define the contracts that are required by the business.
Before thinking about databases caching or infrastructure the Basket service needs to express what operations it supports. These operations are business focused. They describe what the system can do not how it does it.
The repository interface becomes that contract.
public interface IBasketRepository
{
Task<ShoppingCart> GetBasket(string userName, CancellationToken cancellationToken);
Task<ShoppingCart> StoreBasket(ShoppingCart basket, CancellationToken cancellationToken);
Task<bool> DeleteBasket(string userName, CancellationToken cancellationToken);
}
This abstraction turned out to be the key design decision.
Step 2 The initial BasketRepository
The first implementation handled persistence only. No caching. No extra behavior.
public sealed class BasketRepository : IBasketRepository
{
private readonly IDocumentSession _session;
public BasketRepository(IDocumentSession session)
{
_session = session;
}
public async Task<ShoppingCart> GetBasket(string userName, CancellationToken cancellationToken)
{
var basket = await _session.LoadAsync<ShoppingCart>(userName, cancellationToken);
return basket ?? throw new BasketNotFoundException(userName);
}
public async Task<ShoppingCart> StoreBasket(ShoppingCart basket, CancellationToken cancellationToken)
{
_session.Store(basket);
await _session.SaveChangesAsync(cancellationToken);
return basket;
}
public async Task<bool> DeleteBasket(string userName, CancellationToken cancellationToken)
{
_session.Delete<ShoppingCart>(userName);
await _session.SaveChangesAsync(cancellationToken);
return true;
}
}
At this stage the system was clean and simple.
Step 3 The need for caching
As the project evolved I noticed that GetBasket was called very frequently. Reading the same basket repeatedly from the database was unnecessary.
The requirement was clear. Add Redis caching.
The constraint was also clear. Do not change business logic. Do not touch handlers. Do not break existing behavior.
Step 4 Introducing CachedBasketRepository
Instead of modifying BasketRepository I introduced a new class called CachedBasketRepository.

This class wraps the existing repository and adds caching behavior around it. The original repository remains unchanged.
public class CachedBasketRepository
(IBasketRepository repository, IDistributedCache cache)
: IBasketRepository
{
public async Task<ShoppingCart> GetBasket(string userName, CancellationToken cancellationToken = default)
{
var cachedBasket = await cache.GetStringAsync(userName, cancellationToken);
if (!string.IsNullOrEmpty(cachedBasket))
{
return JsonSerializer.Deserialize<ShoppingCart>(cachedBasket)!;
}
var basket = await repository.GetBasket(userName, cancellationToken);
await cache.SetStringAsync(
userName,
JsonSerializer.Serialize(basket),
cancellationToken);
return basket;
}
public async Task<ShoppingCart> StoreBasket(ShoppingCart basket, CancellationToken cancellationToken = default)
{
await repository.StoreBasket(basket, cancellationToken);
await cache.SetStringAsync(
basket.UserName,
JsonSerializer.Serialize(basket),
cancellationToken);
return basket;
}
public async Task<bool> DeleteBasket(string userName, CancellationToken cancellationToken = default)
{
await repository.DeleteBasket(userName, cancellationToken);
await cache.RemoveAsync(userName, cancellationToken);
return true;
}
}
This class does not replace the repository. It decorates it.
Step 5 Wiring the decorator using Scrutor
The final step was to tell the DI container how these pieces fit together.
builder.Services.AddScoped<IBasketRepository, BasketRepository>();
builder.Services.Decorate<IBasketRepository, CachedBasketRepository>();
This single change activated caching across the service.
Handlers continued to inject IBasketRepository. They were unaware that caching existed. That was exactly the goal.
Why this design works so well
This approach keeps responsibilities clean.
BasketRepositoryfocuses only on persistenceCachedBasketRepositoryfocuses only on caching- Business logic remains unchanged
- Infrastructure concerns are isolated
Most importantly caching became optional and reversible. Removing Redis would only require removing the decorator.
What I learned from this change
The most important lesson was that good architecture allows behavior to be added without rewriting code.
Repository and Decorator patterns are not academic ideas. They quietly enable change in real systems. In this case they allowed performance optimization without coupling business logic to infrastructure.
This experience reinforced something I now strongly believe. Good architecture often looks simple. And that simplicity is what makes systems resilient.