Caching
Armazenar o resultado de uma operação custosa para servir requisições futuras mais rápido. Reduz latência, aumenta throughput e diminui a carga sobre banco de dados e serviços de origem — ao custo de lidar com invalidação e consistência.
Intenção
Cache é uma camada de armazenamento mais rápida que guarda resultados de operações custosas para evitar refazê-las. A premissa é simples: se um dado foi calculado ou buscado uma vez e vai ser pedido de novo em breve, armazená-lo perto do consumidor é mais barato do que buscá-lo novamente na origem.
O benefício é triplo: latência menor (o dado chega ao cliente mais rápido), throughput maior (mais requisições atendidas pela mesma capacidade) e carga reduzida na origem (o banco de dados, a API externa ou o serviço de computação recebe menos chamadas). Cache é uma das técnicas de performance mais impactantes disponíveis e aparece em praticamente todas as camadas de um sistema moderno.
Problema
Operações de I/O — acesso a banco de dados, chamadas a APIs externas, leitura de disco — são ordens de magnitude mais lentas do que operações em memória. Quando essas operações custosas são repetidas para cada requisição, o sistema paga o custo integral mesmo que o resultado não tenha mudado.
- Latência acumulada: uma query que leva 50 ms é imperceptível isolada, mas quando uma página faz 20 queries para montar a resposta, o tempo acumulado passa de 1 segundo. Se a maioria dessas queries retorna os mesmos dados para usuários diferentes, o trabalho está sendo repetido desnecessariamente.
- Sobrecarga da origem em picos: um evento que multiplica por dez as requisições multiplica por dez as queries no banco. Sem cache, a infraestrutura de origem precisa ter capacidade para o pico máximo em vez de para a carga média.
- Limites de rate de APIs externas: serviços de terceiros frequentemente impõem limites de requisições por segundo ou por hora. Sem cache das respostas, o limite é atingido mais rapidamente e o sistema começa a receber erros ou bloqueios.
- Computações repetidas: resultados de agregações, rankings, relatórios e transformações pesadas são candidatos diretos: custam caro para computar e raramente mudam a cada requisição.
Como funciona
Hierarquia de cache
Cache existe em múltiplas camadas, cada uma com latência e capacidade distintas. Quanto mais próximo do processador, mais rápido — e menor — o cache.
Mais rápido / Menor capacidade
┌────────────────────────────────────────────────────────────────┐
│ L1/L2/L3 do processador (~1–10 ns) gerenciado pelo CPU │
│ RAM (~100 ns) gerenciada pelo OS │
│ Cache de aplicação — Redis/Memcached (~1 ms via rede LAN) │
│ CDN (~10–50 ms, borda geográfica) gerenciada pelo CDN │
│ Cache do navegador (0 ms se HIT) gerenciado pelo browser │
└────────────────────────────────────────────────────────────────┘
Mais lento / Maior capacidade (origem: banco de dados, API)
Na prática, o desenvolvedor controla as camadas de aplicação para baixo: cache no próprio processo, Redis/Memcached para cache distribuído entre instâncias, CDN para assets e respostas HTTP e cabeçalhos HTTP para o cache do navegador.
Estratégias de escrita e leitura
A estratégia define o relacionamento entre cache e origem: quem popula o cache, quando e com qual garantia de consistência.
CACHE-ASIDE (Lazy Loading) WRITE-THROUGH
Requisição Escrita
│ │
▼ ▼
┌────────┐ HIT ┌────────┐
│ Cache │──────────────► Resposta │ Cache │◄── escrita simultânea
└────┬───┘ └────┬───┘
│ MISS │ escrita simultânea
▼ ▼
┌────────┐ ┌────────┐
│ Banco │ │ Banco │
└────┬───┘ └────────┘
│ popula cache
▼ Cache sempre fresco.
┌────────┐──────────────► Resposta Toda escrita tem latência dupla.
│ Cache │ Cache pode ter dados nunca lidos
└────────┘ (desperdício de memória).
App controla o cache explicitamente.
Cold start: miss na 1ª requisição.
Risco: dados stale se TTL longo.
WRITE-BEHIND (Write-Back) READ-THROUGH
Escrita Requisição
│ │
▼ ▼
┌────────┐◄── escrita imediata ┌────────┐ HIT
│ Cache │ │ Cache │──────────► Resposta
└────────┘ └────┬───┘
│ assíncrono (batch) │ MISS: cache busca na origem
▼ ▼
┌────────┐ ┌────────┐
│ Banco │ │ Banco │
└────────┘ └────┬───┘
│ popula cache
Máxima performance de escrita. ▼
Risco: perda de dados se cache ┌────────┐──────────────► Resposta
cair antes de persistir no banco. │ Cache │
└────────┘
App não distingue cache de origem.
Políticas de eviction
Cache tem capacidade finita. Quando está cheio, algum dado precisa ser removido para dar espaço ao novo. A política de eviction define qual dado é removido:
- LRU (Least Recently Used): remove o dado acessado há mais tempo. É a política padrão da maioria das implementações (Redis, Memcached) porque reflete bem a localidade temporal — dados usados recentemente têm maior probabilidade de serem usados novamente.
- LFU (Least Frequently Used): remove o dado acessado com menor frequência. Melhor para padrões de acesso estáveis onde alguns dados são acessados consistentemente muito mais que outros.
- TTL (Time To Live): não é uma política de eviction de capacidade — é uma validade máxima. O dado expira após um tempo definido, independente de uso. TTL é o mecanismo principal de invalidação temporal e atua junto com LRU/LFU, não no lugar deles.
Cache em diferentes camadas da aplicação
-
Cache do navegador: controlado por cabeçalhos HTTP
Cache-Control,ETageLast-Modified. Evita requisições ao servidor para assets estáticos (JS, CSS, imagens) e respostas que não mudaram. Um HIT de navegador custa zero — a requisição nem sai do dispositivo do usuário. - CDN: caches geograficamente distribuídos próximos aos usuários. Ideais para assets estáticos, respostas de APIs públicas e conteúdo com TTL definido. Reduzem latência ao servir da borda em vez do servidor de origem e absorvem picos de tráfego massivos.
- Cache de aplicação — Redis/Memcached: compartilhado entre instâncias, persiste além do ciclo de vida de uma requisição. Usado para sessões de usuário, resultados de queries custosas, responses de APIs externas, resultados de computações pesadas e dados de configuração lidos com frequência.
- Cache in-process: estruturas em memória no próprio processo da aplicação (HashMap, Map). Latência zero de rede, mas não compartilhado entre instâncias. Adequado para dados de lookup imutáveis carregados na inicialização (tabelas de configuração, enumerações de domínio).
Quando usar
- Dados lidos muito mais do que escritos: páginas de produto em e-commerce, perfis de usuário, configurações de sistema — qualquer dado com proporção de leitura para escrita alta. O cache retorna o valor muitas vezes por cada vez que é atualizado.
- Operações custosas e repetitivas: queries com múltiplos joins, agregações sobre grandes volumes, chamadas a APIs externas com latência alta. Se o resultado serve mais de uma requisição, cacheá-lo divide o custo de produção entre todos os consumidores.
- Conteúdo estático ou semi-estático: assets de frontend (JS, CSS, imagens), respostas de catálogos, listagens que mudam poucas vezes por dia. TTL de horas ou dias maximiza o hit rate sem risco de exibir dados desatualizados de forma crítica.
- Proteção da origem contra picos: quando o banco ou serviço de origem não tem capacidade para o pico máximo de requisições, o cache absorve a carga e serve os dados sem aumentar a capacidade da origem.
Quando evitar
- Dados que mudam a cada requisição: saldos em tempo real, preços que flutuam por segundo, posições de ativos financeiros. Cachear introduz uma janela de inconsistência que pode ter consequências reais.
- Dados com consistência forte obrigatória: em transações bancárias simultâneas, o saldo após um débito deve ser refletido imediatamente para a próxima operação. Consistência eventual não é aceitável em operações financeiras críticas.
- Quando a invalidação é mais complexa que o problema original: se o dado está relacionado a dezenas de outras entidades e qualquer mudança em qualquer uma delas invalida o cache, a lógica de invalidação vira um sistema por si só. Nesses casos, otimizar a query original pode ser mais direto.
- Dados únicos por usuário com volume de usuários muito alto: cachear dados altamente personalizados pode consumir mais memória do que o benefício gerado, já que cada usuário tem sua própria entrada de cache com hit rate próximo de um.
Prós e contras
Prós
- Redução drástica de latência: dados servidos da memória chegam ordens de magnitude mais rápido do que da origem.
- Throughput maior com o mesmo hardware: o banco processa menos queries, podendo atender mais operações de escrita e queries não cacheadas.
- Proteção da origem: o cache absorve picos de leitura, protegendo banco de dados e APIs de sobrecarga.
- Redução de custo em APIs pagas por requisição: rate limits e custos por chamada são diluídos pelo hit rate do cache.
- Disponibilidade parcial: alguns sistemas são configurados para servir dados do cache mesmo quando a origem está indisponível (stale-while-revalidate).
Contras
- Consistência eventual: há uma janela entre a atualização da origem e a atualização do cache onde o dado servido está desatualizado.
- Complexidade de invalidação: definir quando e como invalidar o cache é um dos problemas mais difíceis em engenharia de software. Invalidar cedo demais anula o benefício; tarde demais serve dados stale.
- Cold start: na primeira requisição ou após reinício, o cache está vazio e todas as requisições chegam à origem simultaneamente.
- Infraestrutura adicional: Redis/Memcached têm custo operacional e financeiro, e precisam ser monitorados, mantidos e operados.
- Bugs difíceis de reproduzir: comportamento que depende do estado do cache é difícil de testar, especialmente quando o comportamento varia entre hit e miss.
Armadilhas comuns
1. Cache stampede (thundering herd)
Quando o TTL de uma chave popular expira, todas as requisições que chegam simultaneamente naquele instante encontram um MISS e disparam a operação custosa ao mesmo tempo — exatamente o problema que o cache deveria evitar, agora amplificado. O banco pode receber dezenas ou centenas de queries idênticas num curto intervalo, saturando sob a carga repentina.
Soluções: jitter no TTL (expirar em TTL + valor aleatório, distribuindo as expirações no tempo para evitar que todas as chaves populares expirem juntas), mutex ou lock (a primeira requisição em MISS adquire um lock e popula o cache; as demais aguardam ou recebem o dado stale temporariamente) e probabilistic early expiration (revalidar o cache proativamente antes de expirar, com probabilidade crescente conforme o TTL se aproxima de zero).
2. Dados stale por invalidação esquecida
O dado é atualizado no banco mas o cache continua servindo a versão antiga. Isso acontece quando a invalidação é tratada como detalhe de implementação em vez de parte do contrato da operação de escrita. Com Cache-Aside, cada operação de escrita precisa invalidar explicitamente as chaves afetadas — e essa disciplina precisa ser mantida conforme novas operações são adicionadas ao longo do tempo.
Regra prática: use TTL como safety net (garante que dados stale eventualmente expiram) e invalidação explícita na escrita como mecanismo principal (garante que a expiração acontece no momento certo). Nunca dependa apenas de um dos dois isoladamente.
3. Cache como fonte de verdade
Em Write-Behind mal implementado, as escritas vão ao cache mas o banco não é atualizado de forma confiável. Se o cache cai — por reinício, falha de hardware ou eviction agressivo — os dados são perdidos permanentemente. Cache é uma camada de aceleração, não de persistência. Trate-o como descartável: o sistema deve funcionar corretamente (mais devagar) sem ele.
4. Chaves de cache sem namespace
Um Redis compartilhado entre ambientes (dev, staging, prod) ou entre
features diferentes pode ter colisão de chaves. A chave user:123
no ambiente de dev aponta para dados de teste; em prod, para o usuário real.
A chave product:list de uma feature pode sobrescrever a de outra
feature diferente.
A solução é prefixar todas as chaves com namespace:
prod:catalog:product:list, dev:auth:user:123.
Defina a convenção de nomenclatura, documente-a e aplique de forma consistente
em todo o código que interage com o cache.
Arquiteturas e padrões relacionados
Escalabilidade horizontal e caching são complementares: o cache reduz a carga na origem, adiando ou eliminando a necessidade de escalar o banco verticalmente. Quando o scale out da camada de aplicação é feito, o cache distribuído (Redis centralizado) garante que todas as instâncias compartilhem o mesmo estado em vez de cada uma manter sua própria cópia desincronizada.
No CQRS, o Read Model frequentemente usa cache: as projeções de leitura são dados pré-computados e otimizados para consulta, podendo ser armazenadas em Redis. A atualização do Read Model via eventos de domínio é uma forma estruturada de invalidação — o evento sinaliza que a projeção precisa ser recalculada.
A Event-Driven Architecture oferece um mecanismo natural para invalidação de cache: quando um evento de domínio é publicado (ProdutoAtualizado, EstoqueAlterado), consumidores especializados podem invalidar ou recomputar as entradas de cache afetadas, desacoplando a invalidação da operação de escrita original.