[Spring Boot] Implementing Multiple Cache Managers

Typically, we leverage a single cache for a specific purpose. however, there are scenarios where we need to integrate multiple caching mechanisms, each serving a different role. In this tutorial, I will demonstrate how to configure multiple Cache Managers in Spring Boot and illustrate the integration between the application layer, database, local cache, and remote cache.

In this scenario, we will use both a local cache (Caffeine) and a remote cache (Infinispan) to store query results.

First we’ll start with a simple pom.xml which contains Spring Boot, MySQL, Infinispan server, and Caffeine for local caches.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>spring-boot-multiple-cacheable</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.infinispan</groupId>
                <artifactId>infinispan-bom</artifactId>
                <version>16.0.5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- caches -->
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring-boot3-starter-remote</artifactId>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-jboss-marshalling</artifactId>
        </dependency>

        <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>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layout>JAR</layout>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

its configuration on application.properties

### server
server.port=8080
spring.application.name=spring-boot-multiple-cacheable

## log
logging.level.root=INFO
logging.level.com.edw=DEBUG

spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql=TRACE

## db
spring.datasource.url=jdbc:mysql://localhost:3306/test_db
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=password

## infinispan
jdg.servers=localhost:11222
jdg.userName=admin2
jdg.password=password

## caffeine
caffeine.spec=maximumSize=500,expireAfterAccess=10m

And some Java files,

package com.edw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
package com.edw.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.infinispan.api.annotations.indexing.Indexed;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor

@Indexed

@Entity
@Table(name = "t_employee")
public class Employee implements Serializable {
    @Id
    public Long id;

    @Column(name = "gender")
    public String gender;

    @Column(name = "firstname")
    public String firstname;

    @Column(name = "lastname")
    public String lastname;
}
package com.edw.repository;

import com.edw.model.Employee;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
    List<Employee> findTop10ByFirstnameLikeAndLastnameLikeIgnoreCase(String firstname, String lastname);
}
package com.edw.service;

import com.edw.model.Employee;
import com.edw.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class EmployeeService {

    private final EmployeeRepository employeeRepository;

    public EmployeeService(@Autowired EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Cacheable(value = "query-cache",
            key = "#id",
            cacheManager = "infinispanCacheManager")
    public Employee getEmployee(Long id) {
        return employeeRepository.findById(id).orElse(new Employee());
    }

    @Cacheable(value = "employee-cache",
            key = "#root.methodName + '-' + #firstname + '-' +  #lastname",
            cacheManager = "caffeineCacheManager")
    public List<Employee> findEmployeesByFirstnameAndLastname(String firstname, String lastname) {
        return employeeRepository.findTop10ByFirstnameLikeAndLastnameLikeIgnoreCase(firstname + "%", lastname + "%");
    }
}
package com.edw.controller;

import com.edw.model.Employee;
import com.edw.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RestController
public class EmployeeController {

    private final EmployeeService employeeService;

    public EmployeeController(@Autowired EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @GetMapping("/employee/{id}")
    public ResponseEntity getEmployeeById (@PathVariable Long id) {
        log.info("getEmployeeById");

        Employee employee = employeeService.getEmployee(id);
        if(employee.getId() == null)
            return ResponseEntity.notFound().build();

        return ResponseEntity.ok(employee);
    }

    @GetMapping("/employee/find-by-firstname-and-lastname/{firstname}/{lastname}")
    public ResponseEntity getEmployeeByFirstnameAndLastname (@PathVariable String firstname, @PathVariable String lastname) {
        log.info("getEmployeeByFirstnameAndLastname");

        List<Employee> employees = employeeService.findEmployeesByFirstnameAndLastname(firstname, lastname);
        if(employees.isEmpty())
            return ResponseEntity.notFound().build();

        return ResponseEntity.ok(employees);
    }

}

And this is the most important part, where we configure our Cache connections. We use @Primary on the Infinispan manager to set it as the default, but we use explicit bean names (caffeineCacheManager and infinispanCacheManager) to distinguish them in the service layer.

package com.edw.config;

import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.ClientIntelligence;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.jboss.marshalling.commons.GenericJBossMarshaller;
import org.infinispan.spring.remote.provider.SpringRemoteCacheManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class InfinispanConfiguration {
    @Value("${jdg.servers}")
    private String jdgHostAddress;

    @Value("${jdg.userName}")
    private String userName;

    @Value("${jdg.password}")
    private String password;

    @Primary
    @Bean(name="infinispanCacheManager")
    public CacheManager infinispanCacheManager() {
        return new SpringRemoteCacheManager(remoteCacheManager());
    }

    @Bean
    public RemoteCacheManager remoteCacheManager() {
        return new RemoteCacheManager(
                new ConfigurationBuilder()
                        .addServers(jdgHostAddress)
                        .security().authentication().username(userName).password(password)
                        .clientIntelligence(ClientIntelligence.HASH_DISTRIBUTION_AWARE)
                        .marshaller(new GenericJBossMarshaller())
                        .build());
    }
}
package com.edw.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineConfiguration {

    @Value("${caffeine.spec}")
    private String spec;

    @Bean(name = "caffeineCacheManager")
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager("employee-cache");
        manager.setCaffeine(Caffeine.from(spec));
        return manager;
    }

}

We can try to trigger API call and can see that the first hit will go to the database, but after that it will always go to Cache to reduce the database workload.

$ curl -kv http://localhost:8080/employee/1

Code for this project can be accessed in the below repository,

https://github.com/edwin/spring-boot-multiple-cacheable

Leave a Comment

Your email address will not be published.