Cache é uma das ferramentas mais poderosas para deixar um sistema rápido — e uma das mais fáceis de errar. A famosa frase "as duas coisas mais difíceis em computação são nomear coisas e invalidar cache" existe por um motivo. Saber quando cachear, como invalidar e quando não cachear é esperado de um dev pleno.
Por que cachear
Cache é guardar o resultado de uma operação cara para reusar depois, evitando refazê-la. A operação pode ser uma query pesada, uma chamada a outra API ou um cálculo demorado. A analogia: em vez de ir ao mercado toda vez que quer leite, você guarda na geladeira. O custo é que o leite na geladeira pode vencer — e é aí que mora toda a complexidade.
O padrão cache-aside
O padrão mais comum é o cache-aside (também chamado lazy loading): a aplicação primeiro olha o cache; se o dado estiver lá (hit), usa; se não (miss), busca na origem, guarda no cache e devolve.
function getProduto(id):
valor = cache.get("produto:" + id)
if valor != null:
return valor // cache hit
valor = banco.query(id) // cache miss
cache.set("produto:" + id, valor, ttl=300)
return valor
A vantagem: só é cacheado o que de fato é pedido. A desvantagem: o primeiro acesso a cada item é sempre lento (o miss inicial).
TTL e expiração
O TTL (Time To Live) define por quanto tempo um valor fica no cache antes de expirar automaticamente. É a forma mais simples de evitar dados eternamente desatualizados. Um TTL curto deixa os dados mais frescos mas reduz o aproveitamento; um TTL longo aproveita mais mas arrisca servir dados velhos. Escolher o TTL é equilibrar frescor contra performance.
Estratégias de escrita
O cache-aside trata da leitura. Quando os dados mudam, há estratégias de escrita. A mais direta é o write-through: ao escrever, você atualiza banco e cache juntos, mantendo-os em sincronia. Outras opções incluem invalidar a chave na escrita (deixar o próximo read recarregar). O importante é ter uma resposta para "o que acontece com o cache quando o dado muda?".
Por que invalidação é difícil
O dado em cache é uma cópia, e cópias ficam stale (desatualizadas) quando a origem muda. Você precisa decidir quando descartá-las — e errar para qualquer lado dói: invalidar demais joga fora a vantagem do cache; invalidar de menos serve dados errados. Pior: invalidar a chave certa, no momento certo, em todos os lugares onde ela existe, é genuinamente difícil em sistemas distribuídos.
Cachear é fácil; o difícil é saber a hora exata de jogar fora o dado guardado. Todo bug de "por que está mostrando o valor antigo?" é um problema de invalidação.
Onde fica o cache
| Local | Velocidade | Compartilhado? |
|---|---|---|
| Memória do processo | Altíssima | Não (só aquele servidor) |
| Redis / Memcached | Muito alta | Sim (entre servidores) |
| CDN | Alta (perto do usuário) | Sim (conteúdo público/estático) |
Cache em memória local é o mais rápido mas não escala entre instâncias — daí o Redis ser tão usado: um cache compartilhado que todas as instâncias consultam. Isso conecta com escalabilidade, já que serviços stateless costumam delegar o estado a um cache externo.
Quando NÃO cachear
Cache não é grátis. Evite quando: os dados mudam a cada acesso (cache nunca acerta), exigem precisão absoluta (saldo bancário, estoque crítico), ou são raramente lidos (não compensa a complexidade). No dia a dia, cachear cedo demais é uma fonte comum de bugs sutis — meça primeiro, cacheie depois.
Conclusão
Cache troca frescor por velocidade. Domine o padrão cache-aside, defina TTLs conscientes, tenha uma estratégia de invalidação e saiba reconhecer os casos onde cachear traz mais problema que benefício. É uma alavanca enorme de performance — desde que você respeite a parte difícil.
Caching is one of the most powerful tools for making a system fast — and one of the easiest to get wrong. The famous line "the two hardest things in computer science are naming things and cache invalidation" exists for a reason. Knowing when to cache, how to invalidate and when not to cache is expected of a mid-level dev.
Why cache
Caching means storing the result of an expensive operation so you can reuse it later instead of redoing it. The operation might be a heavy query, a call to another API or a slow computation. The analogy: instead of going to the store every time you want milk, you keep some in the fridge. The cost is that the milk in the fridge can go bad — and that's where all the complexity lives.
The cache-aside pattern
The most common pattern is cache-aside (also called lazy loading): the application looks at the cache first; if the data is there (hit), it uses it; if not (miss), it fetches from the source, stores it in the cache and returns it.
function getProduct(id):
value = cache.get("product:" + id)
if value != null:
return value // cache hit
value = db.query(id) // cache miss
cache.set("product:" + id, value, ttl=300)
return value
The upside: only what is actually requested gets cached. The downside: the first access to each item is always slow (the initial miss).
TTL and expiration
The TTL (Time To Live) defines how long a value stays in the cache before it expires automatically. It's the simplest way to avoid data staying stale forever. A short TTL keeps data fresher but reduces hit rate; a long TTL gets more reuse but risks serving old data. Choosing the TTL is balancing freshness against performance.
Write strategies
Cache-aside deals with reads. When data changes, there are write strategies. The most direct is write-through: when writing, you update the database and the cache together, keeping them in sync. Other options include invalidating the key on write (letting the next read reload it). What matters is having an answer to "what happens to the cache when the data changes?".
Why invalidation is hard
Cached data is a copy, and copies go stale when the source changes. You have to decide when to discard them — and getting it wrong in either direction hurts: invalidating too much throws away the benefit of the cache; invalidating too little serves wrong data. Worse: invalidating the right key, at the right moment, everywhere it exists, is genuinely hard in distributed systems.
Caching is easy; the hard part is knowing exactly when to throw away the stored data. Every "why is it showing the old value?" bug is an invalidation problem.
Where the cache lives
| Location | Speed | Shared? |
|---|---|---|
| Process memory | Extremely high | No (only that server) |
| Redis / Memcached | Very high | Yes (across servers) |
| CDN | High (close to the user) | Yes (public/static content) |
Local in-memory cache is the fastest but doesn't scale across instances — which is why Redis is so widely used: a shared cache that all instances query. This connects with scalability, since stateless services usually delegate state to an external cache.
When NOT to cache
Caching isn't free. Avoid it when: the data changes on every access (the cache never hits), it requires absolute accuracy (bank balance, critical inventory), or it's rarely read (not worth the complexity). Day to day, caching too early is a common source of subtle bugs — measure first, cache later.
Conclusion
Caching trades freshness for speed. Master the cache-aside pattern, set deliberate TTLs, have an invalidation strategy and learn to recognize the cases where caching brings more problems than benefits. It's a huge performance lever — as long as you respect the hard part.