Home | Send Feedback | Share on Bluesky |

Cache support in Spring Boot

Published: 28. July 2025  •  java, spring

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:

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:

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:

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.