Neste post, vamos mostrar como usar o design pattern Decorator para interceptar um componente, adicionar novos comportamentos — como cache e, ao final, ainda rodar o componente original.
Qual a Palavra de Ouro para Performance? CACHE
Se tivesse que escolher uma só palavra para performance, cache é uma das melhores.
Claro, há várias formas de melhorar a performance de um sistema:
- Otimizar código
- Índices e ajustes de queries
- Algoritmos mais eficientes
- Programação paralela
- Upgrade de hardware
Mas em algum momento, especialmente com milhares ou milhões de requisições por minuto, usar cache de forma estratégica é inevitável.
O Problema
Considere esta API:
[ApiController, Route("cars")]
public class CarController : ControllerBase
{
private readonly ICarStore _store;
public CarController(ICarStore store)
{
_store = store;
}
[HttpGet]
public IActionResult Get()
{
return Ok(_store.List());
}
}
A controller usa uma interface ICarStore
, que acessa o banco de dados. Agora, imagine que essa implementação está em uma biblioteca externa e você não pode alterar o código original.
Então vem a pergunta:
Como aplicar cache aqui sem mudar nem a Controller, nem o CarStore original?
🧠 A Solução: Design Pattern Decorator
A estratégia é simples: usar o padrão Decorator.
Passos:
- Criar uma classe nova que implementa
ICarStore
e internamente recebe umICarStore
original, adicionando cache. - Alterar o registro no Dependency Injection para usar essa nova classe.
Passo 1 — Criando o Decorator
public class CarCachingStore<T> : ICarStore
where T : ICarStore
{
private readonly IMemoryCache _memoryCache;
private readonly T _inner;
private readonly ILogger<CarCachingStore<T>> _logger;
public CarCachingStore(IMemoryCache memoryCache, T inner, ILogger<CarCachingStore<T>> logger)
{
_memoryCache = memoryCache;
_inner = inner;
_logger = logger;
}
public IEnumerable<Car> List() { ... }
public Car Get(int id) { ... }
}
Esse decorator envolve o store original e adiciona cache à execução, sem a Controller saber.
Passo 2 — Configurando o DI
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<CarStore>();
services.AddScoped<ICarStore, CarCachingStore<CarStore>>();
}
Aqui, primeiro registramos a implementação real CarStore
, depois usamos ela como dependência do CarCachingStore
. Assim, quando a Controller pedir um ICarStore
, ela recebe a versão com cache.
💡 Código Completo do Decorator
public class CarCachingStore<T> : ICarStore
where T : ICarStore
{
private readonly IMemoryCache _memoryCache;
private readonly T _inner;
private readonly ILogger<CarCachingStore<T>> _logger;
public CarCachingStore(IMemoryCache memoryCache, T inner, ILogger<CarCachingStore<T>> logger)
{
_memoryCache = memoryCache;
_inner = inner;
_logger = logger;
}
public IEnumerable<Car> List()
{
const string key = "Cars";
if (!_memoryCache.TryGetValue(key, out IEnumerable<Car> cars))
{
cars = _inner.List();
if (cars != null)
_memoryCache.Set(key, cars, TimeSpan.FromMinutes(1));
}
return cars;
}
public Car Get(int id)
{
var key = $"{typeof(T).FullName}:{id}";
if (!_memoryCache.TryGetValue(key, out Car car))
{
_logger.LogTrace("Cache miss for {key}", key);
car = _inner.Get(id);
if (car != null)
{
_logger.LogTrace("Caching item for {key}", key);
_memoryCache.Set(key, car, TimeSpan.FromMinutes(1));
}
}
else
{
_logger.LogTrace("Cache hit for {key}", key);
}
return car;
}
}
Quando usar?
Esse padrão é ótimo quando:
- Você não pode mexer no código original
- Quer adicionar cache sem criar novas interfaces como
ICarStoreWithCache
- Precisa controlar por ambiente (ex: usar cache só em produção)
Também é comum usar do mesmo jeito para adicionar logs, métricas ou estratégias de retry.
Quem já usou isso?
Já usei em muitos projetos, principalmente por não alterar a implementação original. Além disso, o IdentityServer4 também usa essa abordagem, permitindo cache com método de extensão na configuração.
Código no GitHub

Veja o projeto completo em: