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:

  1. Criar uma classe nova que implementa ICarStore e internamente recebe um ICarStore original, adicionando cache.
  2. 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:

github.com/brunobritodev/CacheStrategy