Cache Strategies: principais estratégias de cache usadas no back-end

Quando falamos sobre cache, é muito comum pensar logo em Redis.

E faz sentido. Redis é uma das ferramentas mais usadas para cache no back-end. Mas cache não é apenas Redis.

Cache é uma estratégia para evitar trabalho repetido.

A ideia principal é simples: se uma informação já foi buscada, calculada ou processada antes, talvez não faça sentido repetir todo esse trabalho a cada nova requisição.

Em uma aplicação real, o cache pode existir em vários lugares:

  • na memória da aplicação;
  • no Redis;
  • no navegador;
  • em uma CDN;
  • no Nginx;
  • no Apache;
  • no banco de dados;
  • no sistema operacional;
  • e até no próprio processador.

Mas usar cache não é simplesmente “salvar dados temporariamente”. Existe uma decisão importante por trás disso:

  • quando o dado deve entrar no cache;
  • quando ele deve sair;
  • quando ele deve ser atualizado;
  • e por quanto tempo ele pode ser considerado confiável.

É aqui que entram as cache strategies, ou estratégias de cache.


O que são cache strategies?

Cache strategies são formas de definir como uma aplicação vai lidar com cache.

Elas ajudam a responder perguntas como:

  • Quando devo buscar o dado no cache?
  • Quando devo buscar direto no banco de dados?
  • Quando devo salvar algo no cache?
  • Quando devo remover um cache antigo?
  • Quanto tempo esse dado pode ficar armazenado?
  • O que acontece se o cache estiver desatualizado?

Não existe uma única estratégia perfeita para todos os cenários.

Cada abordagem tem vantagens, desvantagens e trade-offs. Em alguns casos, você vai priorizar performance. Em outros, consistência. Em outros, simplicidade.

Por isso, entender as estratégias mais comuns ajuda bastante na hora de tomar decisões melhores no back-end.


1. Cache-Aside ou Lazy Loading

A estratégia Cache-Aside, também chamada de Lazy Loading, é uma das mais comuns em aplicações web.

Nesse modelo, a aplicação tenta buscar o dado primeiro no cache. Se encontrar, retorna esse dado imediatamente. Se não encontrar, busca no banco de dados, salva o resultado no cache e depois retorna para o usuário.

Como funciona

1. A aplicação procura o dado no cache
2. Se encontrar, retorna o dado
3. Se não encontrar, busca no banco de dados
4. Salva o resultado no cache
5. Retorna o dado para o usuário

Exemplo com Redis

async function getUser(userId) {
  const cacheKey = `user:${userId}`;
 
  const cachedUser = await redis.get(cacheKey);
 
  if (cachedUser) {
    return JSON.parse(cachedUser);
  }
 
  const user = await db.users.findById(userId);
 
  await redis.set(cacheKey, JSON.stringify(user), {
    EX: 60 * 10 // 10 minutos
  });
 
  return user;
}

Quando faz sentido usar

Essa estratégia funciona muito bem para dados que são lidos com frequência, mas que não mudam o tempo todo.

Exemplos:

  • perfil de usuário;
  • configurações públicas;
  • listagem de produtos;
  • posts de um blog;
  • dados de dashboard;
  • permissões com TTL curto.

Vantagens

  • É simples de implementar.
  • Evita consultas repetidas ao banco.
  • Dá controle total para a aplicação.
  • Funciona bem com Redis, Memcached ou cache em memória.

Cuidados

O principal cuidado é que a primeira requisição ainda precisa buscar no banco.

Além disso, se o dado mudar no banco e o cache não for atualizado ou removido, a aplicação pode continuar retornando uma informação antiga.

Por isso, Cache-Aside normalmente é usado junto com TTL ou invalidação de cache.


2. TTL: expiração baseada em tempo

TTL significa Time To Live, ou tempo de vida.

Essa é uma das técnicas mais usadas em cache. A ideia é definir por quanto tempo uma informação pode ficar armazenada antes de expirar automaticamente.

Exemplo com Redis

await redis.set("products:home", JSON.stringify(products), {
  EX: 60 * 5 // 5 minutos
});

Nesse exemplo, a chave products:home ficará salva no cache por 5 minutos.

Depois disso, ela expira automaticamente.

Quando faz sentido usar

TTL é muito útil quando você aceita que o dado fique um pouco desatualizado por alguns segundos ou minutos.

Exemplos:

  • listagem de posts;
  • página inicial;
  • catálogo de produtos;
  • rankings;
  • dados públicos;
  • estatísticas não críticas;
  • configurações que mudam pouco.

Vantagens

  • É fácil de implementar.
  • Evita cache infinito.
  • Reduz risco de dados muito antigos.
  • Funciona bem combinado com Cache-Aside.

Cuidados

O problema do TTL é que, durante o tempo de vida do cache, o dado pode ficar desatualizado.

Imagine uma lista de produtos cacheada por 10 minutos. Se um produto for alterado logo depois que o cache foi criado, os usuários ainda podem ver a versão antiga até o cache expirar.

Isso pode ser aceitável para uma listagem pública, mas talvez não seja aceitável para dados críticos.


3. Event-Based Invalidation

A Event-Based Invalidation, ou invalidação baseada em eventos, acontece quando a aplicação remove ou atualiza um cache a partir de uma ação específica.

Por exemplo: se um produto foi atualizado, o cache daquele produto deve ser removido.

Exemplo

async function updateProduct(productId, data) {
  const product = await db.products.update(productId, data);
 
  await redis.del(`product:${productId}`);
  await redis.del("products:home");
 
  return product;
}

Nesse caso, depois que o produto é atualizado no banco, os caches relacionados são removidos.

Na próxima leitura, a aplicação buscará os dados atualizados no banco e salvará novamente no cache.

Quando faz sentido usar

Essa estratégia é útil quando você precisa ter mais controle sobre a consistência dos dados.

Exemplos:

Produto atualizado → remove cache do produto
Usuário alterou perfil → remove cache do usuário
Novo post publicado → remove cache da listagem
Pedido atualizado → remove cache do dashboard

Vantagens

  • Reduz a chance de retornar dados antigos.
  • Dá mais controle sobre a atualização do cache.
  • Funciona muito bem com eventos de domínio.
  • É uma boa combinação com Cache-Aside.

Cuidados

O maior desafio é saber exatamente quais caches devem ser invalidados.

Em sistemas pequenos, isso parece simples. Mas, em sistemas maiores, uma única alteração pode afetar várias chaves.

Por exemplo, atualizar um produto pode afetar:

product:123
products:home
products:category:5
products:search:tenis
dashboard:admin

Ou seja, invalidar cache parece fácil no começo, mas pode se tornar uma parte complexa da arquitetura.


4. Write-Through Cache

No Write-Through Cache, toda vez que a aplicação escreve no banco de dados, ela também atualiza o cache.

Ou seja, o cache é mantido atualizado no momento da escrita.

Como funciona

1. A aplicação atualiza o banco de dados
2. A aplicação atualiza o cache
3. As próximas leituras já usam o cache atualizado

Exemplo

async function updateUser(userId, data) {
  const updatedUser = await db.users.update(userId, data);
 
  await redis.set(
    `user:${userId}`,
    JSON.stringify(updatedUser),
    { EX: 60 * 10 }
  );
 
  return updatedUser;
}

Quando faz sentido usar

Write-Through faz sentido quando você tem dados muito lidos e quer evitar que a próxima leitura precise consultar o banco novamente.

Exemplos:

  • perfil de usuário;
  • configurações;
  • permissões;
  • dados muito acessados depois de uma atualização.

Vantagens

  • Mantém o cache mais atualizado.
  • Reduz cache miss depois de uma escrita.
  • Melhora a performance das leituras seguintes.

Cuidados

Toda escrita fica um pouco mais cara.

Afinal, além de atualizar o banco, você também precisa atualizar o cache.

Outro ponto importante: você precisa pensar no que acontece se a escrita no banco funcionar, mas a atualização do cache falhar.


5. Write-Around Cache

No Write-Around Cache, a aplicação escreve diretamente no banco de dados, mas não atualiza o cache naquele momento.

O cache só será preenchido novamente quando alguém tentar ler aquele dado.

Como funciona

1. A aplicação escreve no banco de dados
2. O cache não é atualizado
3. Na próxima leitura, se não existir cache, busca no banco
4. Depois salva o resultado no cache

Quando faz sentido usar

Essa estratégia é útil quando muitas informações são escritas, mas nem todas serão lidas novamente.

Imagine um sistema que recebe muitos logs, eventos ou registros temporários. Se cada escrita também atualizasse o cache, você poderia encher o cache com dados que talvez ninguém acesse.

Vantagens

  • Evita poluir o cache com dados pouco acessados.
  • Reduz custo durante operações de escrita.
  • Pode ser útil em sistemas com muita escrita.

Cuidados

A primeira leitura depois da escrita pode ser mais lenta, porque precisará consultar o banco.

Além disso, se já existia um cache antigo para aquele dado, você precisa decidir se ele deve ser removido ou não.


6. Write-Back ou Write-Behind Cache

No Write-Back Cache, também conhecido como Write-Behind Cache, a aplicação escreve primeiro no cache. Depois, em outro momento, esse dado é persistido no banco de dados.

Como funciona

1. A aplicação escreve no cache
2. A resposta é retornada rapidamente
3. Depois, o dado é salvo no banco de dados

Essa estratégia pode ser muito performática, porque a aplicação não precisa esperar a escrita no banco para responder.

Quando faz sentido usar

Ela pode ser usada em cenários como:

  • contadores;
  • métricas;
  • logs;
  • eventos temporários;
  • sistemas com alto volume de escrita;
  • dados onde algum atraso na persistência é aceitável.

Um exemplo conceitual seria:

Incrementa contador no Redis
Periodicamente sincroniza o valor com o banco

Vantagens

  • Escritas muito rápidas.
  • Reduz pressão no banco de dados.
  • Pode agrupar várias escritas e persistir depois.

Cuidados

Essa estratégia é mais arriscada.

Se o cache cair antes que os dados sejam persistidos no banco, pode haver perda de informação.

Por isso, ela não costuma ser uma boa escolha para dados críticos, como pagamentos, saldo financeiro ou informações que não podem ser perdidas.


7. Read-Through Cache

No Read-Through Cache, a aplicação pede o dado ao cache, mas não é ela quem busca diretamente no banco caso o cache esteja vazio.

Nesse modelo, o próprio mecanismo de cache sabe como carregar o dado da fonte original.

Como funciona

1. A aplicação pede o dado ao cache
2. O cache verifica se tem o dado
3. Se não tiver, o cache busca na fonte original
4. O cache salva o resultado
5. O cache retorna o dado para a aplicação

Diferença para Cache-Aside

Ele parece bastante com Cache-Aside, mas existe uma diferença importante.

No Cache-Aside, a aplicação controla a lógica de buscar no banco e salvar no cache.

No Read-Through, essa responsabilidade fica abstraída no próprio sistema de cache ou em alguma camada intermediária.

Quando faz sentido usar

Essa estratégia depende bastante da infraestrutura, biblioteca ou ferramenta usada.

Ela pode ser útil quando você quer centralizar a lógica de carregamento dos dados e evitar que cada parte da aplicação implemente sua própria lógica de cache.


8. Refresh-Ahead Cache

No Refresh-Ahead Cache, o sistema atualiza o cache antes que ele expire.

Imagine que um dado tem TTL de 10 minutos. Quando ele estiver próximo de expirar, a aplicação pode atualizar esse cache automaticamente em segundo plano.

O objetivo é evitar que o usuário seja afetado por uma consulta lenta quando o cache expirar.

Como funciona

1. O dado está no cache
2. O sistema percebe que ele está perto de expirar
3. O cache é atualizado antes da expiração
4. O usuário continua recebendo respostas rápidas

Quando faz sentido usar

Essa estratégia é útil para dados muito acessados.

Exemplos:

  • dashboard;
  • ranking;
  • feed público;
  • catálogo de produtos;
  • página inicial;
  • configurações globais.

Vantagens

  • Evita cache miss em dados muito acessados.
  • Melhora a experiência do usuário.
  • Reduz picos de carga no banco quando o cache expira.

Cuidados

Você pode acabar atualizando dados que talvez não sejam mais acessados com tanta frequência.

Por isso, Refresh-Ahead costuma fazer mais sentido para dados realmente importantes e com alto volume de leitura.


9. Stale-While-Revalidate

Stale-While-Revalidate é uma estratégia muito usada em CDNs, aplicações modernas e também pode ser aplicada no back-end.

A ideia é simples: mesmo que o cache esteja vencido, a aplicação pode retornar o dado antigo rapidamente e atualizar o cache em segundo plano.

Como funciona

1. O usuário pede um dado
2. O cache existe, mas está vencido
3. A aplicação retorna o dado antigo mesmo assim
4. Em paralelo, atualiza o cache
5. A próxima requisição recebe o dado atualizado

Exemplo prático

Imagine a página inicial de um blog.

Se o cache da home venceu, talvez não seja necessário fazer o usuário esperar uma nova consulta no banco. Você pode devolver a versão antiga por alguns segundos e atualizar o cache logo em seguida.

Para o usuário, a página carrega rápido.

Para o sistema, você evita uma consulta pesada acontecendo no caminho crítico da requisição.

Quando faz sentido usar

Essa estratégia funciona bem quando não existe problema em mostrar uma informação um pouco desatualizada.

Exemplos:

  • home page;
  • feed público;
  • ranking;
  • estatísticas;
  • listagem de posts;
  • catálogo de produtos.

Cuidados

Ela não deve ser usada para qualquer coisa.

Em dados sensíveis, como saldo bancário, status de pagamento, estoque crítico ou permissões de acesso, retornar uma informação antiga pode gerar problemas sérios.


10. Cache em camadas

Em sistemas reais, normalmente não existe apenas uma camada de cache.

Uma requisição pode passar por várias camadas antes de chegar ao banco de dados.

Exemplo de camadas

Browser Cache

CDN

Nginx / Reverse Proxy

Memória da aplicação

Redis

Banco de dados

Cada camada tem um papel diferente.

O navegador pode armazenar arquivos estáticos. A CDN pode armazenar imagens, scripts, CSS e páginas públicas. O Nginx pode cachear respostas HTTP. A aplicação pode manter dados frequentes em memória. O Redis pode armazenar sessões, consultas pesadas e dados compartilhados entre múltiplas instâncias.

Exemplo prático

Imagem estática → CDN
Página pública → CDN ou Nginx
Sessão do usuário → Redis
Configuração usada toda hora → memória local
Consulta pesada → Redis
Dados persistentes → banco de dados

Quanto mais perto do usuário a resposta for resolvida, menor tende a ser a latência.

Mas quanto mais camadas você adiciona, mais importante fica entender como expirar, invalidar e atualizar cada uma delas.


Estratégias mais comuns na prática

Na maioria das aplicações web, uma combinação bastante comum é:

Cache-Aside + TTL + Invalidação por Evento

Exemplo:

Ao buscar produtos:
- tenta buscar no Redis
- se não encontrar, busca no banco
- salva no cache por alguns minutos
 
Ao atualizar um produto:
- atualiza o banco
- remove os caches relacionados

Essa combinação é simples, eficiente e atende bem muitos cenários.

Mas a escolha depende muito do tipo de dado.


Como escolher a melhor estratégia?

Antes de sair cacheando tudo, vale responder algumas perguntas:

  • Esse dado é lido com frequência?
  • Essa consulta é realmente cara?
  • Esse dado muda muito?
  • Posso retornar uma versão antiga por alguns segundos ou minutos?
  • O que acontece se o cache estiver errado?
  • Como esse cache será invalidado?
  • Esse dado é crítico para o usuário ou para o negócio?

Essas perguntas ajudam a evitar cache desnecessário.

Cache pode melhorar muito a performance, mas também pode criar bugs difíceis de encontrar.

Às vezes, o problema não está no banco, na query ou no Redis. Está em uma chave de cache antiga sendo retornada sem ninguém perceber.


Cuidado: cache também adiciona complexidade

Cache é muito útil, mas não vem de graça.

Alguns problemas comuns são:

  • cache desatualizado;
  • invalidação incorreta;
  • muitas chaves difíceis de gerenciar;
  • consumo excessivo de memória;
  • dados inconsistentes entre cache e banco;
  • dependência excessiva do Redis;
  • dificuldade para debugar respostas antigas.

Por isso, cache não deve ser usado apenas porque parece uma boa prática.

Ele precisa resolver um problema real.

Se uma consulta é simples, rápida e pouco acessada, talvez cachear só adicione complexidade sem trazer muito benefício.


Resumo das principais estratégias

EstratégiaIdeia principalQuando usar
Cache-AsideA aplicação busca no cache e, se não encontrar, busca no bancoDados lidos com frequência
TTLO cache expira depois de um tempo definidoDados que podem ficar levemente antigos
Event-Based InvalidationO cache é removido quando algo mudaDados que precisam de mais consistência
Write-ThroughEscreve no banco e atualiza o cache juntoDados muito lidos após atualização
Write-AroundEscreve no banco, mas não atualiza o cacheSistemas com muitas escritas
Write-BackEscreve primeiro no cache e persiste depoisMétricas, contadores e logs
Read-ThroughO próprio cache carrega o dado da fonte originalInfraestruturas que abstraem carregamento
Refresh-AheadAtualiza o cache antes dele expirarDados muito acessados
Stale-While-RevalidateRetorna dado antigo e atualiza em segundo planoConteúdos públicos e não críticos
Cache em camadasUsa cache em diferentes níveis da arquiteturaSistemas com alta escala

Conclusão

Cache é uma das formas mais eficientes de melhorar a performance de uma aplicação.

Mas cache não é só instalar Redis e salvar qualquer coisa nele.

O ponto principal é entender o comportamento dos dados.

Alguns dados podem ficar alguns minutos desatualizados sem problema. Outros precisam estar corretos o tempo inteiro. Alguns são acessados milhares de vezes por minuto. Outros quase nunca são lidos novamente.

É isso que define a estratégia.

Na prática, muitas aplicações começam com uma combinação simples:

Cache-Aside + TTL + Invalidação por Evento

Essa abordagem já resolve boa parte dos problemas comuns de performance no back-end.

O segredo é lembrar que cache sempre envolve um trade-off entre performance, consistência e complexidade.

Quanto mais performance você busca, mais cuidado precisa ter para não entregar dados antigos, errados ou inconsistentes para o usuário.