Caffeine Cache with Spring Boot

Caffeine is an open-source, high-performance Java caching library providing high hit rates and excellent concurrency. This Spring boot tutorial will teach us to configure and work with Caffeine cache with simple examples.

1. Introduction to Caffeine

Caffeine is the Java 8 successor to ConcurrentLinkedHashMap and Guava’s cache. Caffeine Cache is similar to JDK ConcurrentMap except that it can be configured to evict entries automatically to constrain its memory footprint.

A cache’s eviction policy tries to predict which entries are most likely to be used again in the near future, thereby maximizing the hit ratio. Caffeine uses the Window TinyLfu eviction policy.

Caffeine has three strategies for value eviction:

  • Size-based: Eviction occurs when the configured size limit of the cache is exceeded. The cache will try to evict entries that have not been used recently or very often.
  • Time-based: Caffeine provides three approaches to timed eviction
    • Expire After Access: Expires the entry after the specified duration is passed since the last read or write occurs
    • Expire After Write: Expires the entry after the specified duration is passed since the write occurs
    • Expire After: Custom expiry for each entry based on Expiry implementation.
  • Reference-based: Caffeine allows to set up the cache to allow the garbage collection of entries, by using weak references for keys or values, and soft references for values.

2. Dependency

Let’s configure caching in our Spring Boot application. Since Caffeine is an external library, add the latest versions of com.github.ben-manes.caffeine:caffeine dependency from Maven repo. We’ll also need to add spring-boot-starter-cache to import the Spring Caching support.

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.5</version>
</dependency>

For Gradle, add the following dependency.

implementation group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '3.1.5'

3. Caffeine Cache Configuration

The Spring framework provides support for transparently adding caching to an application. Spring boot autoconfigures the cache infrastructure as long as caching support is enabled via the @EnableCaching annotation.

@EnableCaching
@Configuration
public class CacheConfig {

  //...
}

There are two ways in which Caffeine can be configured in our Spring Boot application.

3.1. Java Configuration

To enable Caffeine cache, we will need to create the Caffeine bean that will control the caching parameters like cache size and expiry. Here we are configuring cache which would have an initial capacity of 10 items and an expiry of 60 seconds.

Note that Spring Boot auto-configures the CaffeineCacheManager if Caffeine is present in the classpath. We can define CaffeineCacheManager bean if we need any customization.

import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@EnableCaching
@Configuration
public class CacheConfig {

  @Bean
  public Caffeine<Object, Object> caffeineConfig() {
    return Caffeine.newBuilder()
        .expireAfterWrite(300, TimeUnit.SECONDS)
        .initialCapacity(10);
  }

  @Bean
  public CacheManager cacheManager(Caffeine caffeine) {
    CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
    caffeineCacheManager.setCaffeine(caffeine);
    return caffeineCacheManager;
  }
}

3.2. Properties Configuration

We can define the cache configuration in application.properties file. Note that when expireAfterWrite and expireAfterAccess coexist, expireAfterWrite takes priority.

spring.cache.cache-names=StudentCache, CourseCache
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=10,expireAfterAccess=300s

Let us walk through how to work with Caffeine when retrieving data from repositories.

4. Save Entities to Cache with @Cacheable

Let’s create a StudentService which will fetch the Student name given an id. We will enable caching on getName() method using @Cacheable annotation. We will also provide a name for this cache where the results would be stored.

For certain results, we can use the unless attribute on the @Cacheable annotation. When the criteria (a SpEL expression) are met, the returned object is not cached. In the following example, if the result is null, the caching will be vetoed.

import com.howtodoinjava.dao.StudentRepository;
import com.howtodoinjava.exception.RecordNotFoundException;
import com.howtodoinjava.model.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class StudentService {

  @Autowired
  private StudentRepository studentRepository;

  @Cacheable("StudentCache", unless="#result == null")
  public Student getName(Long id) {

    return studentRepository.findById(id)
        .orElseThrow(() -> new RecordNotFoundException("Record not wound with id: " + id));
  }
}

Each time the method is invoked, caching behavior will be applied, checking whether the method has already been invoked for the given arguments.

In the following example, we save the StundentEntity and then we fetch it from the database twice. The first time, the entity is fetched from the database and an SQL query is logged. The second time, the entity is found in the cache so no SQL query is executed and thus not logged as well.

@SpringBootApplication
public class App implements CommandLineRunner {

  @Autowired
  StudentService studentService;

  @Autowired
  StudentRepository studentRepository;

  public static void main(String[] args) {

    SpringApplication.run(App.class, args);
  }

  @Override
  public void run(String... args) throws Exception {
    Student student = studentRepository.save(new Student("Lokesh Gupta"));

    student = studentService.getName(student.getId()); //Hits the database
    student = studentService.getName(student.getId()); //Fetched from cache
  }
}

The program output:

Hibernate: select next value for student_seq
Hibernate: insert into student (name,id) values (?,?)

Hibernate: select s1_0.id,s1_0.name from student s1_0 where s1_0.id=?

5. Generating Custom Cache Key Names

By default, Spring Data uses the method params to compute the cache key. If no value is found in the cache for the computed key, the target method will be invoked, and the returned value will be stored in the associated cache.

We can customize the cache name using a SpEL expression via the key attribute, or a custom org.springframework.cache.interceptor.KeyGenerator implementation.

5.1. Using SpEL

Let’s create a new getId() method that will get the Student Id from the student’s name and surname. By default, the cache key will be a combination of both name and surname. If we want to customize it and use only the surname of the cache key, we can do it by specifying the SpEL using the key attribute.

@Cacheable(value = "StudentCache", key="{#surname}")
public int getId(String name, String surname) {
	return studentDAO.fetchId(name, surname);
}

Here, we only use the surname to construct the cache key. Note that if the surnames of two students are the same it may lead to overwrites.

5.2. Using Custom KeyGenerator

We can define a KeyGenerator and pass it to the @Cacheable annotation. A KeyGenerator needs to implement just one single method: generate().

public class SurnameKeyGenerator implements KeyGenerator {

	public Object generate(Object target, Method method, Object... params) {
		return params[1];
	}
}

Let’s create a KeyGenerator bean and name it using a qualifier.

@Bean("surnameKeyGenerator")
public KeyGenerator keyGenerator() {
	return new SurnameKeyGenerator();
}

Use this custom key in the method using the keyGenerator attribute.

@Cacheable(value = "StudentCache", keyGenerator = "surnameKeyGenerator")
public int getId(String name, String surname) {
	return studentDAO.fetchId(name, surname);
}

6. Conditional Caching

We can have a conditional cache by providing a SpEL expression in the condition or unless attribute.

6.1. Using condition Attribute

Using the same method shown above, we will only cache if the surname is not “abc

@Cacheable(value = "StudentCache", condition = "#surname != 'abc'")
public int getId(String name, String surname) {
	return studentDAO.fetchId(name, surname);
}

6.2. Using unless Attribute

Unlike the conditional attribute, unless expressions are evaluated after the method execution. It means we would also have access to the result to construct the unless expressions. Here, we cache unless the surname is “ABC

@Cacheable(value = "StudentCache", unless = "#surname == 'abc'")
public int getId(String name, String surname) {
	return studentDAO.fetchId(name, surname);
}

7. Difference between @Cacheable and @CachePut

The @Cacheable will be executed only once for the given cache key, and subsequent requests won’t execute the method until the cache expires or gets flushed.

The @CachePut, on the other hand, does not cause the advised method to be skipped. Rather, it always causes the method to be invoked and its result to be stored in the associated cache if the condition() and unless() expressions match accordingly.

8. Inspecting Cache Content

Inspecting the cache to see possible cached keys/value may help especially while debugging an application. We can inject the CacheManager in any component to inspect the cache.

@GetMapping(value = "/inspectCache")
public void inspectCache(String cacheName) {

	CaffeineCache caffeineCache = (CaffeineCache) cacheManager.getCache(cacheName);
	Cache<Object, Object> nativeCache = caffeineCache.getNativeCache();

	for (Map.Entry<Object, Object> entry : nativeCache.asMap().entrySet()) {

		System.out.println("Key = " + entry.getKey());
		System.out.println("Value = " + entry.getValue());
	}
}

If we want to list all the cache names known to the CacheManager we can use getCacheNames() which returns a Collection<String>.

cacheManager.getCacheNames();

9. Conclusion

This tutorial taught us about Caffeine Cache and how we can use it with Spring Boot. We learned how to add to the cache conditionally or unconditionally and also generate custom keys. We also learned how to inspect the cache.

Happy Learning!!

Source Code on Github

Comments

Subscribe
Notify of
guest
2 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode