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, etc.).
Dans cet article, vous verrez comment activer le cache, choisir un moteur, utiliser les annotations @Cacheable, @CacheEvict, @CachePut, définir les clés et TTL, exposer des métriques et mettre en place une stratégie d’invalidation.
1) 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.
2) Panorama rapide 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.
3) Démarrage rapide
3.1) 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")
}
3.2) 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 { }
3.3) 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.
4) 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 ; passez à Redis en prod multi-instances.
5) Configuration Caffeine (recommandé en local/simple prod)
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.
6) 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.
7) 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, etc.).
8) 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 (DDDD) :
@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) {}
}
9) Tests du cache
- Test unitaire du
keySpEL et du comportement : utilisez unCacheManagerréel (Caffeine en mémoire) via@SpringBootTestou@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.
10) 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 (vise ≥ 80 % 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"]
11) 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é) → explosion mémoire. - Évitez de mettre en cache des erreurs/
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
En résumé, la mise en place du cache avec Spring Boot apporte des gains concrets et rapides :
- 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
En suivant ces bonnes pratiques, vous améliorerez significativement les performances tout en gardant une dette technique maîtrisée.