Phil Karlton disse que existem dois problemas difíceis na computação: naming, cache invalidation, e off-by-one errors. A piada é velha porque o problema de invalidação é real. Mas isso não significa que você deve evitar cache. Significa que precisa de uma estratégia.

Os três níveis de cache web

Uma aplicação web típica tem três pontos onde cache ajuda:

  • CDN (edge cache): cache estático mais próximo do usuário. Imagens, CSS, JS bundle, páginas pré-renderizadas.
  • Application cache: cache em memória ou Redis para dados que mudam pouco. Sessões, configurações, queries frequentes.
  • Database cache: query results, prepared statements, materialized views.

Cada nível tem características diferentes de invalidação. CDN é o mais agressivo (TTL longo, invalidação manual). Application cache é o mais flexível (TTL curto, invalidação por evento). Database cache é o mais conservador (invalidação automática via triggers ou materialized views).

Redis: o workhorse do application cache

Redis é rápido porque mantém dados em memória. É a escolha correta para dados que precisam ser lidos milhares de vezes por segundo.

// Cache-aside pattern
async function getUser(id: string): Promise<User> {
  const cacheKey = `user:${id}`

  // 1. Tenta cache
  const cached = await redis.get(cacheKey)
  if (cached) return JSON.parse(cached)

  // 2. Cache miss: busca no banco
  const user = await db.user.findUnique({ where: { id } })
  if (!user) throw new AppError('NOT_FOUND', 404, 'Usuário não encontrado')

  // 3. Armazena no cache com TTL
  await redis.setex(cacheKey, 3600, JSON.stringify(user))  // 1 hora

  return user
}

O padrão cache-aside é o mais comum: o código tenta cache primeiro, busca no banco se miss, e armazena no cache para próxima leitura. É simples e funciona para a maioria dos casos.

Invalidation strategies

Os padrões de invalidação que funcionam em produção:

TTL (Time-To-Live)

O cache expira automaticamente após um período. Simples, previsível, aceita dados eventualmente consistentes.

// Dados que mudam raramente: TTL longo
await redis.setex('config:app', 86400, JSON.stringify(config))  // 24h

// Dados que mudam frequentemente: TTL curto
await redis.setex('feed:popular', 300, JSON.stringify(feed))     // 5min

Event-driven invalidation

Quando o dado muda, invalida o cache explicitamente. Requer um evento de domínio que dispara a invalidação.

// Ao atualizar usuário, invalida o cache
async function updateUser(id: string, data: UpdateUserDto): Promise<User> {
  const user = await db.user.update({ where: { id }, data: data })

  // Invalida cache do usuário e listas que o incluem
  await redis.del(`user:${id}`)
  await redis.del('users:active')  // invalida cache da listagem

  return user
}

Write-through cache

Atualiza cache e banco na mesma operação. Garante consistência imediata, mas adiciona latência na escrita.

CDN cache: headers que importam

Controlar cache de CDN requer os headers corretos:

// Cache estático: 1 ano, immutable
Cache-Control: public, max-age=31536000, immutable

// API com dados que mudam
Cache-Control: private, max-age=0, must-revalidate
ETag: "abc123"

// Página que muda por dia
Cache-Control: public, max-age=3600, stale-while-revalidate=86400

stale-while-revalidate permite servir conteúdo stale enquanto revalida em background. O usuário recebe resposta rápida, e o conteúdo é atualizado sem delay perceptível.

Cache warming

Após deploy ou restart, caches ficam vazios. Isso causa um pico de latência (cold start). Cache warming pré-carrega dados frequentes antes que o tráfego chegue:

// No startup do servidor
async function warmCache() {
  const popularProducts = await db.product.findMany({
    orderBy: { views: 'desc' },
    take: 100
  })

  await Promise.all(popularProducts.map(p =>
    redis.setex(`product:${p.id}`, 7200, JSON.stringify(p))
  ))
}

Quando não cachear

Nem tudo precisa de cache. Dados que mudam a cada requisição (saldos em tempo real), dados sensíveis sem controle de acesso adequado, e resultados de queries simples e rápidas não se beneficiam de cache. O overhead de invalidar e manter o cache pode ser maior que o ganho de performance.