Existe uma piada antiga na computação que diz que só há dois problemas difíceis de verdade: invalidar cache, nomear coisas e erros de contagem. A piada é boba, mas o recado sobre cache é sério. Adicionar cache é a coisa mais fácil do mundo. Manter ele coerente com a realidade é onde mora a dor.
O cache resolve um problema real: buscar o mesmo dado caro repetidamente é desperdício. Mas, no instante em que você guarda uma cópia, cria um segundo problema — e agora existem duas versões da verdade que podem discordar.
O trade-off que ninguém escapa
Cache é, no fundo, uma aposta: “esse dado não vai mudar tão rápido, então vale guardar uma cópia por perto”. Quando a aposta acerta, você economiza uma consulta cara e a resposta voa. Quando erra, você serve dado velho — e, dependendo do que for, isso vai de irrelevante a desastroso.
Um preço desatualizado por três segundos numa página de blog: ninguém morre. Um saldo bancário desatualizado, ou um estoque que diz “disponível” quando já esgotou: aí a conta chega. Por isso, a primeira pergunta antes de cachear qualquer coisa é: qual o custo de esse dado estar errado por um tempo?
As estratégias, e o que cada uma cobra
Existem algumas formas clássicas de manter o cache honesto. Nenhuma é a “certa” — cada uma troca uma dor por outra.
Expiração por tempo (TTL)
A mais simples: cada entrada vive por X segundos e depois morre sozinha. É fácil de implementar e previsível. O custo é que você aceita, de propósito, servir dado potencialmente velho durante a janela do TTL. Para muita coisa, é o suficiente. Um TTL de 30 segundos numa listagem de produtos raramente vai incomodar alguém.
Invalidar na escrita
Aqui você é ativo: quando o dado muda, você apaga (ou atualiza) a entrada do cache na mesma hora. Fica bem mais fresco. O preço é o acoplamento — todo lugar que escreve precisa lembrar de mexer no cache. Esquecer um único caminho de escrita é o suficiente para deixar uma cópia velha viva para sempre, e esse tipo de bug é traiçoeiro de rastrear.
func atualizarPreco(id, valor) {
db.update(id, valor)
cache.delete("produto:" + id) // fácil de esquecer
}
A chave que muda com o dado
Um truque elegante é embutir uma “versão” na chave do cache — a data de atualização do registro, por exemplo. Quando o dado muda, a chave muda junto, e a entrada antiga simplesmente deixa de ser consultada (e some sozinha depois). Você troca o problema de invalidar pelo problema de gerar chaves consistentes, o que costuma ser mais fácil de acertar.
As armadilhas que aparecem no pior momento
Além da coerência, cache tem dois problemas operacionais que só se manifestam sob carga — ou seja, exatamente quando você menos quer.
- Estouro em massa (cache stampede). Uma entrada popular expira, e mil requisições que dependiam dela batem no banco ao mesmo tempo para recalcular. O cache que existia para proteger o banco vira o gatilho de uma avalanche. Mitiga-se deixando só a primeira requisição recalcular enquanto as outras esperam.
- Chave inexistente batendo sempre no banco. Se você cacheia resultados mas não cacheia “esse dado não existe”, buscas por IDs inválidos furam o cache toda vez. Vale guardar o “vazio” também, com um TTL curto.
Cache não é uma feature que você liga e esquece. É um sistema paralelo de verdade que precisa combinar com o original — e que vai divergir no dia em que você deixar de prestar atenção.
Cache não vive num lugar só
Um detalhe que confunde bastante gente: existe cache em várias camadas ao mesmo tempo, e elas nem sempre concordam entre si. O navegador guarda uma cópia. Uma CDN na frente guarda outra. A sua aplicação guarda a dela. O banco tem o próprio cache interno. Quando um dado muda, você pode ter limpado o cache da aplicação e ainda estar servindo a versão velha que a CDN memorizou.
Isso explica aquele bug clássico: “atualizei, mas continua aparecendo o antigo — e só para alguns usuários”. Cada camada tem seu próprio ciclo de vida, e o dado velho pode estar preso em qualquer uma delas. Ao desenhar cache, vale mapear quantas cópias da verdade existem no caminho entre o banco e o olho do usuário.
Quanto mais camadas, mais rápido o sistema — e mais lugares para o dado velho se esconder. É o mesmo trade-off de sempre, agora multiplicado.
A regra que economiza sofrimento
Antes de adicionar cache, meça. Muita gente coloca cache por reflexo, para “melhorar performance”, sem nunca ter confirmado que aquele ponto é lento de verdade. Você adiciona uma camada inteira de complexidade — e de bugs de dado velho — para resolver um problema que talvez nem existisse.
Quando o cache for realmente necessário, comece pelo mais simples que resolve: um TTL curto costuma entregar 90% do ganho com 10% da dor. Invalidação ativa e esquemas mais espertos entram quando o dado velho começa a custar caro de verdade. Cache é ótimo servo e péssimo patrão — trate ele com o respeito de quem sabe que, um dia, ele vai te servir a versão errada da verdade.



