Arquitetura de Cache Local com Caffeine
O Caffeine consoildou-se como a principal biblioteca de cache em memória para aplicações Java modernas. Originalmente, o Guava Cache era o padrão da indústria, utilizando algoritmos como LRU e mecanismos de coleta de lixo baseados em referências. No entanto, o Caffeine reescreveu essa arquitetura aproveitando os recursos do Java 8, oferecendo uma taxa de acerto (hit rate) muito superior e um desempenho otimizado. Desde a versão 2.0 do Spring Boot, ele é automaticamente configurado como o gerenciador de cache padrão quando detectado no classpath.
Por que optar por Cache Local?
A principal vantagem do cache em memória local reside na latência. Enquanto soluções distribuídas como o Redis são excelentes para compartilhamento de estado entre múltiplos nós, elas ainda incorrem em overhead de rede, serialização e I/O. Para dados de leitura frequente, alta volatilidade e que não exigem consistência estrita enntre diferentes instâncias da aplicação, o cache local elimina o gargalo de rede, entregando respostas na casa dos nanossegundos.
Configuração de Dependências
Para integrar o Caffeine ao ecossistema Spring, adicione as seguintes dependências ao seu arquivo pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
Definição de Parâmetros via YAML
A maneira mais rápida de configurar o cache é através do arquivo de propriedades do Spring, definindo as especificações diretamente via string de configuração do Caffeine:
spring:
cache:
type: caffeine
cache-names: productCatalog, userSessions
caffeine:
spec: maximumSize=2000,expireAfterAccess=15m,recordStats
Configuração Programática via Java Config
Para um controle mais granular, como a adição de listeners de remoção ou configurações específicas por bean, a abordagem programática é a mais recomendada:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class InventoryCacheSetup {
@Bean("inventoryCache")
public Cache<String, InventoryItem> buildInventoryCache() {
return Caffeine.newBuilder()
.initialCapacity(200)
.maximumSize(5000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener((key, value, cause) ->
System.out.println("Entrada removida - Chave: " + key + " | Motivo: " + cause))
.build();
}
}
Manipulação Manual do Cache
Quando a lógica de negócio exige controle explícito sobre a invalidação ou atualização dos dados, a API nativa do Caffeine pode ser injetada e utilizada diretamente:
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import com.github.benmanes.caffeine.cache.Cache;
@Service
public class InventoryManager {
private final Cache<String, InventoryItem> stockCache;
public InventoryManager(@Qualifier("inventoryCache") Cache<String, InventoryItem> stockCache) {
this.stockCache = stockCache;
}
public void updateStock(String sku, InventoryItem item) {
stockCache.put(sku, item);
}
public InventoryItem fetchStock(String sku) {
// Retorna o valor se presente, ou null caso não exista no cache
return stockCache.getIfPresent(sku);
}
public void clearStock(String sku) {
stockCache.invalidate(sku);
}
}
Abordagem Declarativa com Spring Cache
Para operações CRUD padrão, a abstração de cache do Spring permite desacoplar a lógica de cacheamento do código de negócio através de anotações:
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
@CacheConfig(cacheNames = "articleStore")
public class ArticleManager {
// Simulação de repositório de dados
private final Map<Long, Article> mockDatabase = new ConcurrentHashMap<>();
@CachePut(key = "#article.id")
public Article publishArticle(Article article) {
mockDatabase.put(article.getId(), article);
return article;
}
@Cacheable(key = "#articleId")
public Article findArticle(Long articleId) {
simulateDatabaseLatency();
return mockDatabase.get(articleId);
}
@CacheEvict(key = "#articleId")
public void retractArticle(Long articleId) {
mockDatabase.remove(articleId);
}
private void simulateDatabaseLatency() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}