Le cache est l’un des leviers les plus efficaces pour améliorer la latence et réduire la charge d’une application. Spring Boot fournit une abstraction de cache très puissante, compatible avec plusieurs moteurs (Caffeine, Redis, Ehcache, Hazelcast).
Dans cet article :
- Pourquoi utiliser un cache et quand l’éviter
- L’abstraction Spring Cache et ses annotations
- Configurer Caffeine (en mémoire) et Redis (partagé)
- Définir des clés, conditions et TTL
- Stratégies d’invalidation et tests
- Monitoring avec Actuator et Micrometer
- Pièges courants et bonnes pratiques
Pré-requis : Java 17 ou plus récent et Spring Boot 3.x. Les exemples utilisent Spring Boot 3.3.
Pourquoi mettre du cache ?
- Diminuer la latence des endpoints et batchs.
- Réduire la charge CPU/IO de services internes ou bases de données.
- Lisser les pics de trafic et améliorer la résilience.
- Faire des économies d’infrastructure.
Attention : le cache n’est pas un substitut à un modèle de données ou d’indexation correct. Il complète une conception saine.
Panorama de l’abstraction Spring Cache
L’API Spring Cache fournit :
- Des annotations déclaratives :
@EnableCaching,@Cacheable,@CacheEvict,@CachePut,@Caching. - Un mécanisme de génération de clé (SpEL) et de conditions (
condition,unless). - Une intégration transparente avec différents
CacheManager(Caffeine, Redis, Ehcache).
Le code métier reste identique : seul le backend change via la configuration.
Démarrage rapide
Dépendances Maven
<dependencies>
<!-- API cache Spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>3.3.5</version>
</dependency>
<!-- Choisissez un moteur -->
<!-- Caffeine (en mémoire, très rapide, TTL/size policy) -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- Ou Redis (partagé, scalable) -->
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.3.5</version>
</dependency>
-->
</dependencies>
Gradle (Kotlin DSL) :
dependencies {
implementation("org.springframework.boot:spring-boot-starter-cache:3.3.5")
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
// implementation("org.springframework.boot:spring-boot-starter-data-redis:3.3.5")
}
Activer le cache
Dans votre classe d’application (ou une classe de config) :
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@EnableCaching
@SpringBootApplication
public class Application { }
Première méthode cachée
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class PriceService {
// Cache "prices" par défaut ; clé générée à partir des arguments (SpEL)
@Cacheable(cacheNames = "prices", key = "#productId")
public Price getPrice(String productId) {
return fetchPriceFromSlowApi(productId); // appel coûteux
}
}
- Premier appel : MISS, exécution réelle et mise en cache.
- Appels suivants avec la même clé : HIT.
Choisir un moteur de cache
- Caffeine : en mémoire, ultra-rapide, TTL/size/expire-after-write/access, très simple en mono-process.
- Redis : partagé (cluster/containers), persistant en mémoire, TTL par entrée, idéal multi-réplicas.
- Ehcache, Hazelcast, Infinispan : alternatives JVM, parfois distribuées, selon vos contraintes.
Commencez simple : Caffeine local en dev/POC, puis passez à Redis en prod multi-instances.
Configuration Caffeine
application.yml :
spring:
cache:
cache-names: [prices, products]
caffeine:
spec: maximumSize=10000,expireAfterWrite=10m,recordStats
Déclarer un CacheManager explicite (optionnel si vous utilisez la propriété ci-dessus) :
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager mgr = new CaffeineCacheManager("prices", "products");
mgr.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return mgr;
}
}
Récupérer des stats (Micrometer) :
management:
endpoints.web.exposure.include: ["metrics", "health"]
Puis consultez /actuator/metrics/cache.gets, etc.
Configuration Redis (prod multi-instances)
Dépendances : spring-boot-starter-data-redis (Lettuce par défaut).
application.yml :
spring:
data:
redis:
host: localhost
port: 6379
cache:
type: redis
cache-names: [prices, products]
Configurer TTL par cache et sérialisation :
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.cache.*;
import java.time.Duration;
import java.util.Map;
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory cf) {
GenericJackson2JsonRedisSerializer json = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(json))
.entryTtl(Duration.ofMinutes(10)); // TTL par défaut
Map<String, RedisCacheConfiguration> configs = Map.of(
"prices", defaultConfig.entryTtl(Duration.ofMinutes(15)),
"products", defaultConfig.entryTtl(Duration.ofMinutes(5))
);
return RedisCacheManager.builder(cf)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configs)
.build();
}
}
Remarques :
- Les clés sont des
String; les valeurs sérialisées en JSON (lisibles, évolutives). - Adaptez le TTL à la volatilité métier de la donnée.
Annotations essentielles et SpEL
@Cacheable(cacheNames, key, unless, condition): lit/écrit si absence.@CachePut: force l’écriture sans court-circuiter l’exécution.@CacheEvict(cacheNames, key, allEntries): supprime ; utile après une écriture.@Caching: combiner plusieurs annotations.
Exemples :
// Clé composite avec SpEL
@Cacheable(cacheNames = "productByShop", key = "#shopId + ':' + #productId")
public Product getProduct(String shopId, String productId) { ... }
// Conditionner le cache
@Cacheable(cacheNames = "prices", key = "#id", condition = "#id != null", unless = "#result == null")
public Price price(String id) { ... }
// Invalidation ciblée après update
@CacheEvict(cacheNames = "prices", key = "#p.id")
public Price updatePrice(Price p) { return repo.save(p); }
// Invalidation massive (ex: job de purge)
@CacheEvict(cacheNames = {"prices", "products"}, allEntries = true)
public void clearAllCaches() {}
Astuce : définissez des clés stables et explicites ; évitez celles sensibles aux variations (locales, ordre de paramètres).
Stratégie d’invalidation
La cohérence est clé. Quelques approches :
- Invalidation au plus près des mutations : utilisez
@CacheEvictdans les services qui écrivent. - Écouter des événements domain (DDD) :
@TransactionalEventListenerpour évincer après commit. - TTL raisonnable pour limiter la dérive en cas d’oubli d’invalidation.
- Préremplissage (warmup) des caches les plus chauds au démarrage ou via un job.
Exemple avec événement :
public record PriceChangedEvent(String productId) {}
@Service
public class PriceWriter {
private final ApplicationEventPublisher publisher;
public PriceWriter(ApplicationEventPublisher publisher) { this.publisher = publisher; }
@Transactional
public void updatePrice(Price p) {
repo.save(p);
publisher.publishEvent(new PriceChangedEvent(p.id()));
}
}
@Component
public class PriceCacheInvalidator {
@CacheEvict(cacheNames = "prices", key = "#event.productId")
@TransactionalEventListener
public void onPriceChanged(PriceChangedEvent event) {}
}
Tests du cache
Test unitaire du key SpEL et du comportement : utilisez un CacheManager réel (Caffeine en mémoire) via @SpringBootTest ou @DataJpaTest + import de config. Vidangez le cache entre scénarios si nécessaire.
@SpringBootTest
class PriceServiceTest {
@Autowired PriceService service;
@Autowired CacheManager cacheManager;
@Test
void cached_method_hits_cache() {
String id = "A-42";
service.getPrice(id); // MISS
service.getPrice(id); // HIT
var cache = cacheManager.getCache("prices");
assertThat(cache).isNotNull();
assertThat(cache.get(id)).isNotNull();
}
}
Pour Redis en test : utilisez Testcontainers Redis ou un Redis éphémère.
Monitoring et métriques
Avec Actuator + Micrometer :
cache.gets,cache.puts,cache.evictions,cache.size, latences.- Export Prometheus/Grafana pour visualiser le taux de HIT (visez 80% ou plus sur les chemins chauds).
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: ["health", "metrics", "prometheus"]
Pièges courants et bonnes pratiques
- Ne cachez pas des données hautement sensibles dans un cache partagé sans chiffrement.
- Attention à la cardinalité des clés (ex:
varynon contrôlé) qui peut entraîner une explosion mémoire. - Évitez de mettre en cache des erreurs ou
nullsans TTL réduit ou garde-fou (unless). - Pour les applications multi-instances, évitez le cache en mémoire seul ; préférez Redis.
- Pensez au versioning de schéma de vos objets mis en cache (compatibilité JSON lors des déploiements progressifs).
- Définissez une politique de TTL par type de donnée, documentée et mesurable.
Conclusion
La mise en place du cache avec Spring Boot apporte des gains concrets et rapides.
Points clés à retenir :
- Commencez simple avec Caffeine en local et mesurez les gains sur 2-3 méthodes coûteuses
- En production multi-instances, migrez vers Redis pour un cache partagé et consistant
- Mettez en place une stratégie d’invalidation au plus près des écritures avec
@CacheEvict - Définissez des TTL adaptés à la volatilité de chaque type de donnée
- Surveillez les taux de hit/miss et la taille des caches via les métriques Actuator
- Restez vigilant sur la cardinalité des clés et la sécurité des données sensibles
Pour aller plus loin
- Documentation Spring Cache
- Spring Boot - Cache auto-configuration
- Caffeine
- Spring Data Redis
- Micrometer