[POUPER] Otimizando nossas seções com Caffeine

Estamos perto do nosso lançamento oficial (12/11/2025) e, com isso, vem a fase de otimização de performance. Queremos garantir que, mesmo com o crescimento no número de usuários, a experiência continue rápida e fluida. Uma das áreas que identificamos como um ponto de alto tráfego em nosso backend (feito em Java 21 com Spring Boot) é o sistema de seções de itens de mercado. Especificamente, temos "seções públicas" (como "Frutas", "Padaria", "Limpeza") que são visíveis para todos os usuários. Em um cenário de pico, se 10.000 usuários acessarem o app, faríamos 10.000 requisições idênticas ao banco de dados. Isso é um gargalo óbvio. Para resolvê-lo, adotamos o Caffeine.
O que é o Caffeine?
Quem já trabalhou com ConcurrentMap sabe que ele pode servir como um cache simples. Mas a diferença fundamental é que ele não gerencia a memória — tudo que entra, só sai se você remover manualmente. O Caffeine, por outro lado, é uma biblioteca de cache moderna e de altíssimo desempenho para Java. Desenvolvida por Ben Manes, ela é o sucessor natural do cache do Guava, e é usada em larga escala por empresas como LinkedIn, Netflix e até o próprio Spring Framework.
A filosofia do Caffeine é simples: fornecer um cache quase ótimo, com custo amortizado O(1) para inserções e leituras, mesmo sob alta concorrência. Entre suas funcionalidades principais:
- Carregamento automático de dados (sincrônico ou assíncrono);
- Expiração baseada em tempo (expireAfterWrite, expireAfterAccess);
- Evicção por tamanho, com base na frequência e recência de uso;
- Notificação de remoção (removalListener);
- Integração com métricas (Micrometer, Prometheus, etc.);
- Estatísticas detalhadas via recordStats() (hits, misses, tempo médio, etc.);
- Suporte a referências fracas e suaves para liberar memória sob pressão do GC.
Por que o Caffeine é tão eficiente?
Quando o cache atinge seu limite, ele precisa decidir quem sai para dar lugar a quem entra — isso é o algoritmo de substituição (eviction policy). Os mais conhecidos são:
- LRU (Least Recently Used) – remove o item usado há mais tempo;
- LFU (Least Frequently Used) – remove o item menos acessado.
Cada um tem suas falhas:
- LRU é rápido, mas se engana com acessos temporários.
- LFU é inteligente, mas lento para se adaptar a novos padrões.
O Caffeine combina o melhor dos dois com o algoritmo Window TinyLFU.
O TinyLFU mantém um contador probabilístico de quantas vezes uma chave foi acessada utilizando estruturas como o Count-Min Sketch, que ocupam pouquíssima memória. Por isso o nome Tiny: ele é “minúsculo” em consumo de memória, mas preciso o suficiente para distinguir entre dados quentes e frios. Assim, o cache aprende quais dados são mais valiosos com base em frequência, sem precisar armazenar contadores para tudo.
A Window é uma pequena área LRU no início do cache (teste). Todo item novo entra nela. Se for acessado novamente, “ganha crédito” no TinyLFU e pode ser promovido para a área principal. Se não for, é rapidamente removido, sem contaminar o cache.
Dito isso, o cache do Caffeine é dividido em três regiões principais:
- Window → área LRU de entrada para novos itens;
- Main (Protected) → área principal, gerenciada por TinyLFU + Segmented LRU;
- Probation → área intermediária para itens recém-promovidos.
Quando o cache está cheio, o Caffeine compara a frequência estimada do novo item com a de um antigo. Se o novo for mais “quente”, substitui o velho. Se não, é descartado.
O resultado é um cache que se adapta ao padrão de acesso em tempo real.

Nosso caso: cacheando getPublicSections
No backend da Pouper, o método abaixo busca todas as seções públicas no banco:
@Service
@RequiredArgsConstructor
public class SectionService {
private final SectionRepository sectionRepository;
private final MapToSectionResponseDTO mapToResponseDTO;
@Cacheable("publicSections")
public List<SectionResponseDTO> getPublicSections() {
return sectionRepository.findAllByIsPublicTrue().stream()
.map(mapToResponseDTO::execute)
.collect(Collectors.toList());
}
}
A anotação @Cacheable("publicSections") é o ponto mágico:
na primeira execução, o resultado é buscado do banco e armazenado no cache.
A partir da segunda chamada, a resposta vem direto da memória — em microssegundos, sem query no banco.
O Spring Boot já possui integração nativa com o Caffeine. Basta criar uma configuração simples:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(50)
.maximumSize(150)
.expireAfterAccess(10, TimeUnit.MINUTES)
.recordStats();
}
@Bean
public CacheManagerCustomizer<CaffeineCacheManager> cacheManagerCustomizer() {
return cacheManager -> cacheManager.setCacheNames(List.of("publicSections"));
}
}
Aqui, definimos duas coisas importantes:
-
caffeineCacheBuilder → define o comportamento do cache (capacidade, expiração, estatísticas). O uso de expireAfterAccess(10, TimeUnit.MINUTES) garante que, se ninguém acessar as seções por 10 minutos, a entrada é removida automaticamente.
-
cacheManagerCustomizer → registra o cache “publicSections” na inicialização da aplicação, garantindo que ele já exista antes da primeira chamada.
🔧 O impacto em alguns testes
Após aplicar o cache com o Caffeine no método getPublicSections, fizemos uma bateria de testes de carga em ambiente de staging, simulando 10.000 requisições concorrentes usando o Gatling.
| Métrica | Sem Cache | Com Caffeine |
|---|---|---|
| Tempo médio de resposta | 47 ms | 4,8 ms |
| Taxa de acerto (hit rate) | — | 98,9% |
| Carga média no banco (queries/s) | 1000 | 11 |
| CPU média do backend | 82% | 41% |
Esses resultados nos dão confiança de que o backend está pronto para lidar com o crescimento de tráfego esperado no lançamento, sem sobrecarregar o banco de dados e mantendo uma experiência fluida para o usuário.
Fontes e material de estudo:
https://github.com/ben-manes/caffeine
https://www.baeldung.com/java-caching-caffeine
https://highscalability.com/design-of-a-modern-cache/
https://www.tabnews.com.br/BrunoFerreira44/como-criar-conteudo-no-tabnews-com-imagens