In this blog post, we'll look at the caching support in Spring Boot. Caching has been part of the Spring Framework since version 3.1 (December 2011). Over time, it has evolved to provide a powerful and flexible caching abstraction that can be used with various cache providers. Spring Boot, on top of the Spring Framework, simplifies the setup and configuration of caching in applications.
Quick setup ¶
Spring Boot provides a declarative model, where caching is applied through annotations. Here is a simple example.
First, we need to enable cache support with the @EnableCaching
annotation in a configuration class.
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class Application {
// ...
}
Next, we need to annotate the methods we want to cache with the @Cacheable
annotation.
The value of the @Cacheable
annotation specifies the name of the cache.
By default, the arguments of the method are used to generate the cache key, and the return value is stored as the value in the cache.
import org.springframework.cache.annotation.Cacheable;
@Service
public class Calculator {
@Cacheable("calculator")
public long max(long a, long b) {
// ...
}
}
If we call this method with the same arguments multiple times, the result will be cached after the first call, and subsequent calls will return the cached value without executing the method again.
calculator.max(42, 10); // Executes the method and caches the result
calculator.max(42, 10); // Returns the cached value without executing the method
calculator.max(42, 11); // Executes the method again, as the arguments are different
Pitfall ¶
As mentioned above, the arguments form the cache key by default. Let's see what happens when we add a second method with the same cache name and the same number of parameters.
@Cacheable("calculator")
public long min(long x, long y) {
// ...
}
If we call the max
method first and then the min
method with the same arguments, we will get an unexpected result
because both methods will use the same cache and cache key.
calculator.max(42, 10); // Executes the method and caches the result
calculator.min(42, 10); // Returns the cached value of the `max` method instead of executing the `min` method
This behavior occurs only when the parameters of both methods have the same type. The name of the parameter is not relevant here.
To solve this issue, you could specify different cache names.
@Cacheable("calculatorMax")
public long max(long a, long b) {
// ...
}
@Cacheable("calculatorMin")
public long min(long x, long y) {
// ...
}
Another way is to use a custom key generator. The default algorithm works as follows:
- If no parameters are given, return
SimpleKey.EMPTY
- If only one parameter is given, return that instance
- If more than one parameter is given, return a
SimpleKey
that contains all parameters
With a custom key generator, you can implement your own logic for generating cache keys.
Key Generation ¶
There are different ways to write and configure a custom key generator.
A simple way is to set the key
attribute of the @Cacheable
annotation to a SpEL expression.
Here we add the name of the method and the parameters to the key.
@Cacheable(cacheNames = "calculator", key = "{ #root.methodName, #x, #y }")
public long min(long x, long y) {
// ...
}
@Cacheable(cacheNames = "calculator", key = "{ #root.methodName, #p0, #p1 }")
public long max(long a, long b) {
// ...
}
You can reference the parameters by their name (e.g., #x
, #y
) or by their index with either #a0
, #a1
, or #p0
, #p1
.
In the documentation you find all available variables in the SpEL context.
Another way to write and configure a key generator is to implement the KeyGenerator
interface and register it as a bean in the Spring context.
@Component
public class MethodNameKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
List<Object> elements = new ArrayList<>();
elements.add(method.getName());
for (Object param : params) {
elements.add(param);
}
return new SimpleKey(elements.toArray());
}
}
You can then reference this key generator in the @Cacheable
annotation by specifying the keyGenerator
attribute.
@Cacheable(cacheNames = "calculator", keyGenerator = "methodNameKeyGenerator")
public long max(long a, long b) {
// ...
}
@Cacheable(cacheNames = "calculator", keyGenerator = "methodNameKeyGenerator")
public long min(long x, long y) {
// ...
}
Instead of adding the key generator to each method, you can also use the @CacheConfig
annotation at the class level to
specify a default key generator for all methods in the class. You can also use this annotation for setting the cache name.
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
@Service
@CacheConfig(keyGenerator = "methodNameKeyGenerator", cacheNames = "calculator")
public class Calculator {
@Cacheable
public long max(long a, long b) {
// ...
}
@Cacheable
public long min(long x, long y) {
// ...
}
Another way is to configure the key generator globally in the Spring configuration class by implementing the CachingConfigurer
interface.
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class Application implements ApplicationRunner, CachingConfigurer {
@Override
public KeyGenerator keyGenerator() {
return new MethodNameKeyGenerator();
}
}
This key generator will then be used for all methods annotated with @Cacheable
in the application,
unless a specific key generator is specified in the annotation.
Spring Cache Annotations ¶
Spring provides several annotations for declarative caching:
@Cacheable
: Triggers cache population. If the cache already contains a value for the key, the method is not executed, and the value is returned from the cache.@CacheEvict
: Triggers cache eviction, removing entries from the cache.@CachePut
: Updates the cache without preventing method execution. The method always runs, and its result is placed in the cache.@Caching
: Allows grouping multiple cache operations on a single method.@CacheConfig
: A class-level annotation for sharing common cache settings (e.g., cache names, key generator).
JSR-107 (JCache) annotations are also supported: @CacheResult
, @CachePut
, @CacheRemove
, @CacheRemoveAll
, @CacheDefaults
, @CacheKey
, and @CacheValue
. Check the Spring documentation for more details on JSR-107 support and the differences between JSR-107 and Spring's annotations.
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Caching;
@Service
@CacheConfig(cacheNames = "calculator") // Default cache name for all cache operations in this class
public class Calculator {
@CachePut
public BigInteger factorial(long n) {
// Calculate factorial and return the result. Method is always executed, result is stored in the cache
}
@CacheEvict
public void clearCache(long n) {
// Clear the cache entry for the given key
}
@CacheEvict(beforeInvocation = true)
public void clearCacheBeforeExecution(long n) {
// Clear the cache entry before the method is invoked
}
@Caching(evict = {
@CacheEvict,
@CacheEvict(value = "anotherCache", allEntries = true)
})
public void clearAllCaches(long n) {
// Clears the cache entry for the given key in the `calculator` cache and all entries in the `anotherCache`
}
}
You can specify multiple caches for a single operation. In this case, each of the caches is checked before invoking the method — if at least one cache is hit, the associated value is returned. All the other caches that do not contain the value are also updated, even though the cached method was not actually invoked.
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {
// ...
}
Proxy ¶
Cache annotations work through AOP proxies. This means:
- Method visibility: Only
public
methods can be cached when using proxy mode - Self-invocation: Methods calling other cached methods within the same class won't trigger caching
For self-invocation scenarios, consider using AspectJ mode:
@EnableCaching(mode = AdviceMode.ASPECTJ)
public class Application {
// ...
}
Configuration CacheManager ¶
As seen above, configuring caching in Spring Boot is straightforward. Add @EnableCaching
to a configuration class and annotate
methods.
By default, if there are no cache implementations on the classpath, Spring Boot automatically instantiates a org.springframework.cache.concurrent.ConcurrentMapCacheManager
bean, which stores cache entries in a simple in-memory map.
This may be useful for testing or simple caching scenarios.
This cache manager supports a lazy and strict mode. In lazy mode, caches are created on-demand when they are first accessed. In strict mode, all caches must be defined upfront in the configuration.
To enable strict mode, you create the CacheManager bean explicitly in your configuration class, specifying the cache names you want to use in the constructor of ConcurrentMapCacheManager
.
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("calculator", "anotherCache");
}
Instead of programmatically configuring the cache manager, you can also define the cache names in application.properties
or application.yml
:
spring.cache.cache-names=calculator,users
I prefer strict mode, as it helps catch typos and configuration errors early by ensuring that only pre-defined caches can be used.
Advanced cache managers ¶
While the ConcurrentMapCacheManager
is useful for simple scenarios, you may want to use a more advanced cache manager that supports features like expiration, eviction, or distributed caching. Spring Boot supports several cache providers out of the box. You only need to add the appropriate dependency to your project, and Spring Boot will automatically configure the cache manager for you. Check out this section in the Spring Boot documentation for a list of supported cache providers and their dependencies.
A popular choice is Caffeine for an in-memory cache. Add the following dependency to your pom.xml
:
<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>
The easiest way to configure Caffeine is to configure the caches in application.properties
or application.yml
:
spring.cache.type=caffeine
spring.cache.cache-names=calculator,users
spring.cache.caffeine.spec=initialCapacity=10,maximumSize=100,expireAfterWrite=5s,recordStats
Like the ConcurrentMapCacheManager
, Caffeine supports lazy and strict modes. When you list the cache names, as in this example, the cache manager is in strict mode and will throw an exception if you try to access a cache that is not defined in the configuration.
For lazy mode, you can omit the cache-names
property, and caches will be created on-demand when accessed. These on-demand caches will be created with
the specification defined in the spring.cache.caffeine.spec
property.
This wiki page provides an overview of the Caffeine configuration options: Specification
You can also programmatically configure Caffeine in your configuration class:
@Bean
Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.initialCapacity(10)
.maximumSize(100)
.expireAfterWrite(5, TimeUnit.SECONDS)
.recordStats();
}
@Bean
CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeine);
// enable strict mode
cacheManager.setCacheNames(List.of("calculator", "users"));
return cacheManager;
}
For even more advanced scenarios, where each cache requires different configurations, you can use a CacheManagerCustomizer
to register custom caches with specific configurations.
@Bean
CacheManagerCustomizer<CaffeineCacheManager> caffeineCacheManagerCustomizer() {
return manager -> {
manager.registerCustomCache("users", Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build());
manager.registerCustomCache("calculator", Caffeine.newBuilder()
.maximumSize(2000)
.expireAfterAccess(30, TimeUnit.MINUTES)
.recordStats()
.build());
// strict mode
manager.setCacheNames(List.of("calculator", "users"));
// or
// set default cache configuration for lazy mode
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500)
.expireAfterAccess(10, TimeUnit.MINUTES)
.recordStats());
};
}
Distributed cache manager with Hazelcast ¶
Caffeine is an in-memory cache and does not support distributed caching. It's great for high-performance caching within a single application instance. If multiple instances of your Spring Boot application need to share a cache, you can use a distributed cache based on Hazelcast.
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId>
</dependency>
To configure Hazelcast as a cache manager in Spring Boot, you can create a Config
bean that defines the Hazelcast instance and its settings. Here is an example configuration:
@Bean
Config hazelcastConfig() {
Config config = new Config();
config.setInstanceName("my-spring-boot-hazelcast-instance");
// Network Configuration (Example: TCP/IP for a fixed cluster)
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPort(5701).setPortAutoIncrement(true);
JoinConfig joinConfig = networkConfig.getJoin();
joinConfig.getMulticastConfig().setEnabled(false); // Disable multicast
TcpIpConfig tcpIpConfig = joinConfig.getTcpIpConfig();
tcpIpConfig.setEnabled(true);
tcpIpConfig.setMembers(Collections.singletonList("127.0.0.1"));
// Map Configurations for your caches
MapConfig myCacheMapConfig = new MapConfig("calculator");
myCacheMapConfig.setTimeToLiveSeconds(300); // 5 minutes
myCacheMapConfig.setMaxIdleSeconds(180); // 3 minutes
myCacheMapConfig.getEvictionConfig().setEvictionPolicy(EvictionPolicy.LRU)
.setSize(1000)
.setMaxSizePolicy(com.hazelcast.config.MaxSizePolicy.PER_NODE);
config.addMapConfig(myCacheMapConfig);
MapConfig anotherCacheMapConfig = new MapConfig("user");
anotherCacheMapConfig.setTimeToLiveSeconds(600); // 10 minutes
config.addMapConfig(anotherCacheMapConfig);
return config;
}
// Optional: If you want to explicitly define the CacheManager bean.
// Spring Boot auto-configures this if a HazelcastInstance is found.
@Bean
CacheManager cacheManager(HazelcastInstance hazelcastInstance) {
return new HazelcastCacheManager(hazelcastInstance);
}
The Hazelcast cache manager does not enforce strict mode. If there is a map config with the same name as the cache, it will use that configuration. If there is no such map config, it will create a default IMap lazily when the cache is first accessed.
Advanced Cache Configuration ¶
In this section we will look at some advanced configuration options for caching in Spring Boot.
Conditional Caching ¶
You can use the condition
attribute with a SpEL expression to enable caching only if a certain condition is met:
@Cacheable(value = "calculator", condition = "#n > 10")
public BigInteger factorial(long n) {
// ...
}
The unless
attribute can be used to veto caching after the method has been executed, based on the result. For example, to prevent caching null
values:
@Cacheable(value = "calculator", unless = "#result == null")
public BigInteger factorial(long n) { /* ... */ }
Note that you can globally enable or disable null
value caching.
@Bean
public CacheManagerCustomizer<ConcurrentMapCacheManager> cacheManagerCustomizer() {
return (cacheManager) -> cacheManager.setAllowNullValues(false);
}
The cache abstraction also supports java.util.Optional
return types. If an Optional
value is present, it will be stored in the associated cache.
If an Optional
value is not present, null
will be stored in the associated cache (if enabled).
#result
in a SpEL expression always refers to the wrapped value.
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
Note that #result
refers to Book
and not Optional<Book>
. Since it might be null
, we use SpEL's safe navigation operator (?.
).
Synchronized Caching ¶
For multithreaded environments, you can use @Cacheable(sync = true)
to ensure that only one thread computes a value for a given key at a time. Other threads will be blocked until the value is available in the cache. Not all cache providers support this feature.
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {
// ...
}
Caching with CompletableFuture ¶
You can also cache the result of asynchronous methods that return CompletableFuture
and reactive types (Mono, Flux).
The cache will store the future, and when the future is completed, the result will be cached.
@Cacheable("books")
public CompletableFuture<Book> findBook(ISBN isbn) {...}
In order for this to work, the cache manager must support asynchronous retrieval. The default cache manager
ConcurrentMapCacheManager
automatically adapts to this style of retrieval. The CaffeineCacheManager
also supports it, but it must be enabled explicitly by setting setAsyncCacheMode(true)
on the CaffeineCacheManager
instance.
@Bean
CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeine);
cacheManager.setAsyncCacheMode(true);
return cacheManager;
}
Conclusion ¶
Spring's caching abstraction provides a powerful and flexible way to add caching to your applications. Spring Boot simplifies the setup and configuration of caching. With support for multiple cache providers, declarative annotations, and comprehensive configuration options, you can implement effective caching strategies that improve application performance.