Cache — System Design
Cache é uma das técnicas mais importantes para melhorar a performance e a escalabilidade de sistemas.
A ideia é simples:
Se uma informação é acessada frequentemente, podemos armazená-la temporariamente em um local mais rápido para evitar recalcular ou buscar novamente a mesma informação.
Em vez de fazer:
Client
│
▼
API
│
▼
Database
│
▼
Busca dados
│
▼
Retorna resposta
Podemos utilizar um cache:
Client
│
▼
API
│
▼
Cache
│
├── HIT ──► Retorna imediatamente
│
└── MISS
│
▼
Database
│
▼
Cache
│
▼
Resposta
O objetivo é reduzir:
- Latência
- Carga no banco
- Número de operações custosas
- Custos de infraestrutura
Ao mesmo tempo, o cache pode aumentar significativamente a capacidade de um sistema.
1. Por que precisamos de Cache?
Imagine uma aplicação com:
1.000.000 usuários
E cada usuário acessa:
GET /products/123
Se todas as requisições forem diretamente para o banco:
1.000.000 Requests
│
▼
Database
O banco pode se tornar um gargalo.
Agora adicionamos cache:
1.000.000 Requests
│
▼
Cache
│
├── 950.000 → HIT
│
└── 50.000 → MISS
│
▼
Database
Agora o banco recebe apenas uma pequena parte das requisições.
Isso pode melhorar significativamente a escalabilidade.
2. Cache Hit e Cache Miss
Existem dois resultados principais ao consultar um cache.
Cache Hit
O dado está disponível.
GET product:123
│
▼
Cache
│
▼
FOUND
│
▼
RETURN
Isso é um:
Cache Hit
Cache Miss
O dado não está no cache.
GET product:123
│
▼
Cache
│
▼
NOT FOUND
│
▼
Database
Isso é um:
Cache Miss
A aplicação então busca o dado na fonte original e pode armazená-lo no cache.
Database
│
▼
Cache
│
▼
Response
3. Cache Hit Rate
Uma métrica muito importante é o Cache Hit Rate.
Imagine:
1.000 requisições
Dessas:
900 → Cache Hit
100 → Cache Miss
Temos:
Hit Rate = 90%
Quanto maior o Hit Rate, maior a quantidade de requisições atendidas diretamente pelo cache.
Uma aplicação pode monitorar:
Cache Hit Rate
Cache Miss Rate
Cache Evictions
Cache Latency
Memory Usage
Um Hit Rate baixo pode indicar:
- TTL muito curto
- Chaves mal definidas
- Cache pequeno
- Dados pouco reutilizados
- Estratégia de cache inadequada
4. Cache vs Database
Uma diferença fundamental:
DATABASE
é a fonte persistente dos dados.
Enquanto:
CACHE
é normalmente uma cópia temporária ou derivada.
Podemos pensar:
Database
│
│ Fonte original
▼
Cache
│
│ Cópia temporária
▼
Application
Se o cache desaparecer:
Cache
X
o sistema ainda pode buscar os dados no banco.
Por isso, normalmente:
Não devemos tratar o cache como a única fonte de verdade.
5. Onde o Cache pode existir?
Existem diferentes níveis de cache.
Podemos ter:
Client
│
▼
Browser Cache
│
▼
CDN Cache
│
▼
Application Cache
│
▼
Distributed Cache
│
▼
Database
Cada camada reduz a necessidade de acessar a próxima.
6. Browser Cache
O navegador pode armazenar recursos localmente.
Por exemplo:
HTML
CSS
JavaScript
Imagens
Fonts
Em vez de baixar novamente:
Browser
│
▼
Cache local
│
▼
Arquivo
Podemos utilizar headers HTTP como:
Cache-Control: max-age=3600
Isso informa que o recurso pode ser armazenado por determinado período.
7. CDN Cache
CDNs armazenam conteúdo próximo dos usuários.
Imagine:
Usuário Brasil
│
▼
CDN São Paulo
│
▼
Cache Hit
Sem cache:
Brasil
│
▼
Servidor EUA
Com CDN:
Brasil
│
▼
CDN
│
▼
Resposta
Isso reduz:
- Latência
- Tráfego até o servidor de origem
- Carga na aplicação
CDNs são especialmente úteis para:
- Imagens
- Vídeos
- JavaScript
- CSS
- Arquivos estáticos
8. Application Cache
Podemos armazenar dados na memória da aplicação.
Por exemplo:
API Server
│
├── Cache local
│
└── Database
Isso é extremamente rápido.
Porém, existe um problema.
Imagine:
Load Balancer
│
┌──────┼──────┐
▼ ▼ ▼
App 1 App 2 App 3
Cada servidor possui seu próprio cache:
App 1 → Cache A
App 2 → Cache B
App 3 → Cache C
Agora podemos ter:
App 1 → product:123 = R$ 100
App 2 → product:123 = R$ 120
Os caches podem divergir.
Por isso, em sistemas distribuídos, muitas vezes utilizamos um cache compartilhado.
9. Distributed Cache
Uma solução comum é utilizar um sistema de cache distribuído.
Exemplos:
- Redis
- Memcached
Arquitetura:
LOAD BALANCER
│
┌───────────┼───────────┐
▼ ▼ ▼
API 1 API 2 API 3
│ │ │
└───────────┼───────────┘
│
▼
REDIS
│
▼
DATABASE
Agora todas as instâncias utilizam o mesmo cache.
API 1 ──┐
API 2 ──┼──► Redis
API 3 ──┘
Isso é especialmente útil para aplicações Stateless.
10. Redis
O Redis é uma das soluções mais utilizadas para caching.
Ele armazena dados principalmente em memória.
Isso permite operações extremamente rápidas.
Podemos armazenar:
user:123
product:456
session:abc
Por exemplo:
SET product:123 "Produto A"
Depois:
GET product:123
Retorna:
Produto A
O Redis também oferece estruturas de dados como:
- Strings
- Lists
- Sets
- Sorted Sets
- Hashes
- Streams
Isso permite utilizar Redis para mais do que apenas cache.
11. Cache-Aside
Uma das estratégias mais comuns é:
Cache-Aside
Também chamada de:
Lazy Loading
O fluxo é:
Application
│
▼
Check Cache
│
├── HIT ──► Return
│
└── MISS
│
▼
Database
│
▼
Update Cache
│
▼
Return
Exemplo:
GET product:123
A aplicação verifica:
Redis
Se encontrar:
HIT
Retorna.
Se não encontrar:
MISS
Busca no banco:
Database
│
▼
Product 123
Depois salva:
Redis
│
▼
product:123
12. Vantagens do Cache-Aside
É simples de implementar.
O cache armazena apenas dados que realmente são utilizados.
Database
│
▼
Request
│
▼
Cache
Se um dado nunca for acessado:
Não entra no cache
Isso economiza memória.
13. Desvantagens do Cache-Aside
Existe uma pequena complexidade de sincronização.
Imagine:
Database
product:123 = R$ 100
Cache
product:123 = R$ 100
Atualizamos o banco:
Database
product:123 = R$ 120
Mas esquecemos de atualizar o cache:
Cache
product:123 = R$ 100
Agora temos:
Database → R$ 120
Cache → R$ 100
O sistema pode retornar dados antigos.
Esse problema é conhecido como:
Stale Data
14. Write-Through Cache
No modelo Write-Through:
Application
│
▼
Cache
│
▼
Database
Quando escrevemos:
WRITE
O cache é atualizado e o banco também.
Fluxo:
Application
│
▼
Cache
│
▼
Database
A vantagem é manter o cache mais consistente com o banco.
Porém, cada escrita pode ter um custo maior.
15. Write-Back
No modelo Write-Back:
Application
│
▼
Cache
│
│ assíncrono
▼
Database
A aplicação escreve primeiro no cache.
O banco é atualizado posteriormente.
Vantagem:
Escrita mais rápida
Porém:
Cache
X
Se o cache falhar antes de persistir os dados:
Dados podem ser perdidos
Por isso, essa estratégia é mais arriscada quando os dados são críticos.
16. Cache Invalidation
Existe uma famosa frase atribuída a Phil Karlton:
"There are only two hard things in Computer Science: cache invalidation and naming things."
Invalidar cache significa remover ou atualizar dados antigos.
Imagine:
Cache
product:123 = R$ 100
Atualizamos:
Database
product:123 = R$ 120
Precisamos invalidar:
Cache
product:123
Podemos:
DEL product:123
Na próxima consulta:
Cache MISS
│
▼
Database
│
▼
Novo valor
17. TTL — Time To Live
Uma estratégia comum é definir um tempo de expiração.
Por exemplo:
product:123
TTL = 60 segundos
Depois de 60 segundos:
Cache
│
▼
Expirado
A próxima requisição buscará novamente no banco.
Isso reduz o risco de dados antigos permanecerem indefinidamente.
18. TTL e Stale Data
Imagine:
Database = R$ 120
Cache = R$ 100
TTL = 60 segundos
Durante os próximos 60 segundos:
Usuário
│
▼
Cache
│
▼
R$ 100
Mesmo que o banco contenha:
R$ 120
O cache pode continuar retornando:
R$ 100
Portanto:
TTL reduz a duração do dado stale, mas não elimina completamente a inconsistência.
19. Cache Stampede
Imagine que uma chave popular expire:
product:123
Milhares de usuários fazem requisições simultaneamente:
Request 1 ──┐
Request 2 ──┤
Request 3 ──┤
Request 4 ──┼──► Cache MISS
Request 5 ──┤
Request 6 ──┘
Todos consultam o banco:
DATABASE
▲ ▲ ▲ ▲ ▲ ▲
│ │ │ │ │ │
Requests
Isso pode sobrecarregar o banco.
Esse problema é conhecido como:
Cache Stampede
Ou:
Thundering Herd
20. Como evitar Cache Stampede?
Algumas estratégias:
Lock
Apenas uma requisição atualiza o cache.
Request 1 ──► LOCK ──► Database
Request 2 ──► WAIT
Request 3 ──► WAIT
Request 4 ──► WAIT
Depois:
Database
│
▼
Cache atualizado
│
▼
Requests recebem o resultado
Probabilistic Early Expiration
Podemos atualizar o cache antes de ele realmente expirar.
TTL = 60s
Após 50s
│
▼
Refresh antecipado
Isso evita que milhares de requisições encontrem um cache vazio simultaneamente.
Background Refresh
Um worker atualiza o cache periodicamente:
Worker
│
▼
Database
│
▼
Cache
Assim, os usuários normalmente encontram os dados disponíveis.
21. Cache Penetration
Outro problema ocorre quando usuários consultam repetidamente dados que não existem.
Por exemplo:
GET product:999999999
O banco responde:
NOT FOUND
Como o dado não existe, não armazenamos nada.
Então:
Request
│
▼
Cache MISS
│
▼
Database
│
▼
NOT FOUND
Se milhares de requisições fizerem isso:
Cache MISS
Cache MISS
Cache MISS
Cache MISS
Cache MISS
O banco pode ser sobrecarregado.
Uma solução é armazenar temporariamente o resultado negativo:
product:999999999 = NULL
TTL = 60s
Agora:
Request
│
▼
Cache
│
▼
NULL
Isso é conhecido como:
Negative Caching
22. Cache Avalanche
Imagine que temos milhares de chaves:
Cache A → TTL 60s
Cache B → TTL 60s
Cache C → TTL 60s
Cache D → TTL 60s
Se todas expirarem ao mesmo tempo:
60 segundos
│
▼
TODOS EXPIRAM
Teremos:
Milhares de Cache Misses
│
▼
Database
│
▼
Sobrecarga
Isso é conhecido como:
Cache Avalanche
Uma estratégia comum é adicionar TTL aleatório:
TTL = 60s + Random(0-30s)
Assim, as expirações são distribuídas no tempo.
23. Eviction Policies
O cache possui memória limitada.
Imagine:
Cache
100 GB
Os dados continuam sendo adicionados:
100 GB
101 GB
102 GB
Em algum momento, precisamos remover dados.
Isso é chamado de:
Eviction
Algumas estratégias comuns:
LRU
Least Recently Used
Remove os dados que não são utilizados há mais tempo.
A → usado agora
B → usado recentemente
C → não usado há muito tempo
Remove:
C
LFU
Least Frequently Used
Remove os dados menos acessados.
A → 1.000 acessos
B → 500 acessos
C → 2 acessos
Remove:
C
FIFO
First In, First Out
Remove primeiro os dados mais antigos.
A → entrou primeiro
B
C → entrou por último
Remove:
A
24. Hot Keys
Imagine que temos:
product:123
Esse produto é extremamente popular.
Milhões de requisições:
Request
Request
Request
Request
Request
│
▼
product:123
Essa chave é chamada de:
Hot Key
Ela pode sobrecarregar uma única instância do cache.
Uma solução pode ser replicar o valor:
product:123:A
product:123:B
product:123:C
E distribuir as leituras.
Outra opção é utilizar cache local na aplicação para dados extremamente populares.
25. Quando não devemos usar Cache?
Cache não é uma solução universal.
Ele pode não ser útil quando:
Dados raramente acessados
1 consulta por mês
O cache pode apenas consumir memória.
Dados mudam constantemente
Valor muda
100 vezes por segundo
Manter o cache atualizado pode ser mais complexo do que consultar diretamente a fonte.
Dados precisam de consistência imediata
Se o usuário exige:
Sempre o valor mais recente
o cache pode introduzir dados stale.
Nesse caso, precisamos avaliar cuidadosamente a estratégia.
26. Cache e Consistência
O cache adiciona uma nova cópia dos dados:
Database
│
├── Valor A
│
▼
Cache
│
└── Valor A
Agora temos duas fontes de informação.
Se o banco mudar:
Database = B
Cache = A
Temos inconsistência.
Por isso:
Cache é uma decisão de consistência, não apenas de performance.
Quanto mais agressivo o cache:
Performance ↑
maior pode ser:
Staleness ↑
Esse é um dos principais trade-offs.
27. Arquitetura Completa
Uma arquitetura web moderna pode utilizar múltiplas camadas de cache:
CLIENT
│
▼
BROWSER CACHE
│
▼
CDN
│
▼
LOAD BALANCER
│
┌───────────┼───────────┐
▼ ▼ ▼
API 1 API 2 API 3
│ │ │
└───────────┼───────────┘
│
▼
REDIS
│
▼
DATABASE
Cada camada reduz a carga da próxima.
Browser
│
│ Cache Hit
▼
CDN
│
│ Cache Hit
▼
Redis
│
│ Cache Hit
▼
Database
Idealmente:
Quanto mais próximo do usuário,
mais rápido o acesso.
28. Cache como ferramenta de escalabilidade
O cache não serve apenas para deixar o sistema mais rápido.
Ele também permite reduzir a carga em componentes mais caros.
Imagine:
Sem Cache
100.000 Requests
│
▼
Database
Com cache:
100.000 Requests
│
▼
Redis
│
├── 95.000 HIT
│
└── 5.000 MISS
│
▼
Database
Agora o banco recebe:
5.000 requisições
em vez de:
100.000 requisições
Isso permite que o sistema suporte uma carga maior.
29. Cache e escalabilidade horizontal
Imagine:
LOAD BALANCER
│
┌─────────┼─────────┐
▼ ▼ ▼
API 1 API 2 API 3
│ │ │
└─────────┼─────────┘
│
▼
Redis
│
▼
Database
Podemos adicionar:
API 4
API 5
API 6
Sem precisar duplicar o cache.
Todas continuam utilizando:
Redis
Isso torna o cache distribuído uma peça importante em arquiteturas escaláveis.
30. Cache e o princípio de "source of truth"
Uma arquitetura saudável normalmente define:
Database
│
▼
Source of Truth
E:
Cache
│
▼
Cópia temporária
Isso significa que:
Cache pode desaparecer
Sem perder os dados permanentes.
Se o Redis cair:
Redis
X
podemos temporariamente sofrer:
Cache Miss ↑
Database Load ↑
Latency ↑
Mas os dados continuam no banco.
Esse é um dos motivos pelos quais precisamos pensar em:
- Cache fallback
- Circuit breakers
- Rate limiting
- Replicação
- Alta disponibilidade
31. Checklist para usar Cache
Antes de adicionar cache, pergunte:
O dado é acessado frequentemente?
Sim → Cache pode ajudar
Não → Talvez não valha a pena
O dado muda com frequência?
Muito → Cuidado com stale data
Pouco → Excelente candidato
Qual a tolerância a dados antigos?
Segundos?
Minutos?
Nunca?
Qual será o TTL?
10 segundos?
5 minutos?
1 hora?
O que acontece quando o cache falha?
Database fallback?
Erro?
Qual será o tamanho?
Quantas chaves?
Quantos GB?
Como invalidar?
TTL?
DELETE?
Write-through?
Eventos?
Existe risco de Cache Stampede?
Sim → Lock / Refresh antecipado
Existem Hot Keys?
Sim → Replicação / Cache local
32. Resumo
Cache
Armazena dados temporariamente para acelerar acessos.
Request
│
▼
Cache
│
├── HIT → Resposta
│
└── MISS → Database
Cache Hit
Dado encontrado.
Cache HIT
Cache Miss
Dado não encontrado.
Cache MISS
Cache-Aside
A aplicação controla o cache.
App
│
▼
Cache
│
└── MISS
│
▼
Database
│
▼
Cache
Write-Through
Escreve no cache e no banco.
App
│
▼
Cache
│
▼
Database
Write-Back
Escreve primeiro no cache.
App
│
▼
Cache
│
│ async
▼
Database
TTL
Define quando um item expira.
TTL
│
▼
Expiração
│
▼
Cache Miss
Cache Stampede
Muitas requisições encontram o cache vazio simultaneamente.
Cache MISS
Cache MISS
Cache MISS
Cache MISS
│
▼
Database
Cache Penetration
Muitas consultas por dados inexistentes.
Solução:
Negative Caching
Cache Avalanche
Muitos itens expiram simultaneamente.
Solução:
TTL + Randomização
Conclusão
Cache é uma das ferramentas mais poderosas de System Design.
Ele pode transformar:
100.000 requisições
│
▼
Database
em:
100.000 requisições
│
▼
Cache
│
├── 95.000 → HIT
│
└── 5.000 → Database
O resultado pode ser:
Latência ↓
Database Load ↓
Custo ↓
Throughput ↑
Escalabilidade ↑
Mas cache também introduz complexidade.
Agora precisamos lidar com:
Stale Data
Cache Invalidation
Cache Stampede
Cache Penetration
Cache Avalanche
Hot Keys
Eviction
Por isso, a pergunta não deve ser apenas:
"Posso colocar Redis aqui?"
A pergunta correta é:
"Qual dado vale a pena armazenar em cache, por quanto tempo, qual é a tolerância a dados desatualizados e o que acontece quando o cache falhar?"
Um bom sistema de cache é aquele que entende o comportamento dos dados e escolhe a estratégia adequada.
A regra mental mais importante é:
DADO FREQUENTE
+
DADO QUE PODE SER REUTILIZADO
+
TOLERÂNCIA A STALE DATA
│
▼
CACHE
E, principalmente:
Cache não é uma substituição do banco de dados. É uma camada de aceleração entre a aplicação e a fonte de verdade.
Quando bem utilizado, ele permite que sistemas suportem uma quantidade muito maior de usuários e requisições sem precisar aumentar proporcionalmente a capacidade do banco de dados.