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