"Cache é a diferença entre uma API lenta e uma API instantânea. A estratégia certa reduz latência em 90%." — #30DiasJava Performance Notes
🎯 Objetivo do Day 15
Com o gateway protegendo a entrada, era hora de otimizar o desempenho. O Day 15 do #30DiasJava entregou um sistema de cache completo com múltiplas estratégias, invalidação inteligente e observabilidade de hit rates.
🛠️ O que foi implementado
✅ Múltiplas Estratégias de Cache
- Cache-aside (Lazy Loading): Aplicação gerencia cache manualmente
- Write-through: Escrita simultânea em cache e banco
- Write-behind (Write-back): Escrita assíncrona em batch
- Refresh-ahead: Pré-carregamento antes de expirar
- Cache invalidation: Invalidação por eventos ou TTL
✅ Stack de Cache Multi-Camada
- L1 Cache (Caffeine): Cache in-memory local (ultra-rápido)
- L2 Cache (Redis): Cache distribuído compartilhado
- L3 Cache (Database): Queries otimizadas com índices
- Cache warming: Pré-carregamento em startup
✅ TTL Inteligente
- Static TTL: Tempo fixo para dados estáticos
- Dynamic TTL: TTL baseado em volatilidade dos dados
- Sliding TTL: Renovação automática em acesso
- TTL por tipo: Diferentes TTLs para diferentes domínios
✅ Cache Invalidation
- Event-driven invalidation: Invalidação por eventos de domínio
- Pattern-based invalidation: Invalidação por padrões (ex.:
user:*) - Distributed invalidation: Pub/Sub Redis para invalidar em todos os pods
- Stale-while-revalidate: Serve stale data enquanto revalida em background
✅ Observabilidade
- Hit rate metrics:
cache.hits.total,cache.misses.total - Cache size metrics:
cache.size{name} - Eviction metrics:
cache.evictions.total - Latency metrics:
cache.latency.seconds
📊 Arquitetura
┌─────────────┐
│ Request │
└──────┬──────┘
│
▼
┌─────────────────┐
│ @Cacheable │
│ (Caffeine L1) │
└──────┬──────────┘
│ Miss
▼
┌─────────────────┐
│ Redis Cache │
│ (L2) │
└──────┬──────────┘
│ Miss
▼
┌─────────────────┐
│ Database │
│ (L3) │
└─────────────────┘
🔧 Implementação Técnica
1. Dependências (pom.xml)
<!-- Spring Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine (L1 Cache) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Redis (L2 Cache) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Cache Metrics -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
2. Cache Configuration
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withCacheConfiguration("users",
defaultConfig.entryTtl(Duration.ofHours(1)))
.withCacheConfiguration("families",
defaultConfig.entryTtl(Duration.ofMinutes(30)))
.build();
}
@Bean
public CaffeineCache localCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
}
}
3. Cache Usage
@Service
public class FamiliaService {
@Cacheable(value = "families", key = "#id")
public FamiliaDTO findById(Long id) {
return familiaRepository.findById(id)
.map(this::toDTO)
.orElseThrow();
}
@CacheEvict(value = "families", key = "#id")
public FamiliaDTO update(Long id, FamiliaDTO dto) {
// Update logic
return updated;
}
@CacheEvict(value = "families", allEntries = true)
public void evictAll() {
// Clear all cache
}
}
4. Cache Warming
@Component
@Slf4j
public class CacheWarmer implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private FamiliaService familiaService;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
log.info("Warming cache...");
// Pre-load frequently accessed data
familiaService.findAll().forEach(f -> {
familiaService.findById(f.getId());
});
log.info("Cache warmed successfully");
}
}
💡 Padrões de Cache
Cache-Aside (Lazy Loading)
public FamiliaDTO findById(Long id) {
// 1. Check cache
FamiliaDTO cached = cache.get(id);
if (cached != null) {
return cached;
}
// 2. Load from database
FamiliaDTO dto = loadFromDatabase(id);
// 3. Store in cache
cache.put(id, dto);
return dto;
}
Write-Through
@CachePut(value = "families", key = "#dto.id")
public FamiliaDTO save(FamiliaDTO dto) {
// Write to database and cache simultaneously
return familiaRepository.save(toEntity(dto));
}
Write-Behind (Async)
@Async
@CachePut(value = "families", key = "#dto.id")
public CompletableFuture<FamiliaDTO> saveAsync(FamiliaDTO dto) {
// Write to cache immediately
cache.put(dto.getId(), dto);
// Write to database asynchronously
return CompletableFuture.supplyAsync(() ->
familiaRepository.save(toEntity(dto))
);
}
💡 Lições do dia
- Multi-layer cache maximiza performance: L1 para ultra-rápido, L2 para compartilhado
- TTL é uma arte: Muito curto = muitos misses, muito longo = dados stale
- Invalidation é crítico: Cache stale pode causar bugs graves
- Hit rate é a métrica principal: 80%+ hit rate é excelente
- Cache warming reduz cold starts: Pré-carregamento melhora primeira resposta
📈 Métricas Importantes
- Hit Rate:
cache.hits / (cache.hits + cache.misses) - Cache Size: Tamanho atual do cache
- Eviction Rate: Taxa de remoção por falta de espaço
- Latency: Redução de latência com cache vs sem cache
🔗 Recursos
- Repositório: https://github.com/adelmonsouza/30DiasJava-Day15-Caching
- Spring Cache: https://docs.spring.io/spring-framework/reference/integration/cache.html
- Caffeine: https://github.com/ben-manes/caffeine
- Redis: https://redis.io/docs/
- Projeto pessoal: Este é um projeto do desafio #30DiasJava, mantido independentemente para fins educacionais.
Next episode → Day 16/30 — Config Service & Centralized Configuration