Spring Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll learn how we can configure multiple cache managers in a Spring application.

2. Caching

Spring applies caching to methods so that our application doesn't execute the same method multiple times for the same input.

It's very easy to implement caching in a Spring application. This can be done by adding the @EnableCaching annotation in our configuration class:

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig {}

Then we can start caching the output of a method by adding the @Cacheable annotation on the method:

@Cacheable(cacheNames = "customers")
public Customer getCustomerDetail(Integer customerId) {
    return customerDetailRepository.getCustomerDetail(customerId);
}

As soon as we add the above configuration, Spring Boot itself creates a cache manager for us.

By default, it uses ConcurrentHashMap as the underlying cache if we've not specified any other explicitly.

3. Configuring Multiple Cache Managers

In some cases, we might need to use more than one cache manager in our application. So, let's see how we can do this in our Spring Boot application using an example.

In our example, we'll use a CaffeineCacheManager and a simple ConcurrentMapCacheManager.

CaffeineCacheManager is provided by the spring-boot-starter-cache starter. It'll be auto-configured by Spring if Caffeine is present, which is a caching library written in Java 8.

ConcurrentMapCacheManager uses an implementation of the cache using ConcurrentHashMap.

We can do this in the following ways.

3.1. Using @Primary

We can create two beans of cache managers in our configuration class. Then, we can make one bean primary:

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig {

    @Bean
    @Primary
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager("customerOrders", "orderprice");
    }
}

Now, Spring Boot will use CaffeineCacheManager as default for all the methods until we explicitly specify our alternateCacheManager for a method:

@Cacheable(cacheNames = "customers")
public Customer getCustomerDetail(Integer customerId) {
    return customerDetailRepository.getCustomerDetail(customerId);
}

@Cacheable(cacheNames = "customerOrders", cacheManager = "alternateCacheManager")
public List<Order> getCustomerOrders(Integer customerId) {
    return customerDetailRepository.getCustomerOrders(customerId);
}

In the above example, our application will use CaffeineCacheManager for the getCustomerDetail() method. And for the getCustomerOrders() method, it'll use alternateCacheManager. 

3.2. Extending CachingConfigurerSupport

Another way we can do this is by extending the CachingConfigurerSupport class and by overriding the cacheManager() method. This method returns a bean which will be the default cache manager for our application:

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager("customerOrders", "orderprice");
    }
}

Note that we can still create another bean called alternateCacheManager. We can use this alternateCacheManager for a method by explicitly specifying it, as we'd in the last example.

3.3. Using CacheResolver

We can implement the CacheResolver interface and create a custom CacheResolver:

public class MultipleCacheResolver implements CacheResolver {
    
    private final CacheManager simpleCacheManager;
    private final CacheManager caffeineCacheManager;    
    private static final String ORDER_CACHE = "orders";    
    private static final String ORDER_PRICE_CACHE = "orderprice";
    
    public MultipleCacheResolver(CacheManager simpleCacheManager,CacheManager caffeineCacheManager) {
        this.simpleCacheManager = simpleCacheManager;
        this.caffeineCacheManager=caffeineCacheManager;
        
    }

    @Override
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        Collection<Cache> caches = new ArrayList<Cache>();
        if ("getOrderDetail".equals(context.getMethod().getName())) {
            caches.add(caffeineCacheManager.getCache(ORDER_CACHE));
        } else {
            caches.add(simpleCacheManager.getCache(ORDER_PRICE_CACHE));
        }
        return caches;
    }
}

In this case, we've got to override the resolveCaches method of the CacheResolver interface.

In our example, we're selecting a cache manager based on the method name. After this, we need to create a bean of our custom CacheResolver:

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }

    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager("customerOrders", "orderprice");
    }

    @Bean
    public CacheResolver cacheResolver() {
        return new MultipleCacheResolver(alternateCacheManager(), cacheManager());
    }
}

Now we can use our custom CacheResolver to resolve a cache manager for our methods:

@Component
public class OrderDetailBO {

    @Autowired
    private OrderDetailRepository orderDetailRepository;

    @Cacheable(cacheNames = "orders", cacheResolver = "cacheResolver")
    public Order getOrderDetail(Integer orderId) {
        return orderDetailRepository.getOrderDetail(orderId);
    }

    @Cacheable(cacheNames = "orderprice", cacheResolver = "cacheResolver")
    public double getOrderPrice(Integer orderId) {
        return orderDetailRepository.getOrderPrice(orderId);
    }
}

Here, we're passing the name of our CacheResolver bean in the cacheResolver element.

4. Conclusion

In this article, we learned how we can enable caching in our Spring Boot application. Then, we learned three ways by which we can use multiple cache managers in our application.

As always, the code for these examples is available over on GitHub.

Spring bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
amiamartian
amiamartian
2 months ago

What are the benefits of 3.2 and 3.3 vs 3.1 which seems the simplest possible as it leverages simple framework support on the call site and requires little to no custom code?
Also no mention of https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/cache/support/CompositeCacheManager.html

Loredana Crusoveanu
2 months ago
Reply to  amiamartian

Hi,
We always try to present a few different ways to achieve more or less the same goal. You should always pick the one that suits your needs best.
Using CacheResolver for example, you can select the appropriate CacheManager dynamically at runtime. This is not possible if you go with the first option.
Thanks for suggesting CompositeCacheManager. We’ll update the article.
Cheers

Comments are closed on this article!