Slow performance is a recurring and complex problem that developers are often faced with. One of the most common approaches to address such a problem is through caching. Indeed, this mechanism allows achieving a substantial improvement in the performance of any type of application. The problem is that dealing with caching is not an easy task. Luckily, caching is provided by Spring Boot transparently thanks to the Spring Boot Cache Abstraction, which is a mechanism allowing consistent use of various caching methods with minimal impact on the code. Let's see everything you should know to start dealing with it.
First, we will introduce the concept of caching. Then, we will study the most common Spring Boot cache-related annotations, understanding what the most important ones are, where, and how to use them. Next, it will be time to see what are the most popular cache engines supported by Spring Boot at the time of writing. Finally, we will see Spring Boot caching in action through an example.
What is Caching
Caching is a mechanism aimed at enhancing the performance of any kind of application. It relies on a cache, which can be seen as a temporary fast access software or hardware component that stores data to reduce the time required to serve future requests related to the same data. Dealing with caching is complex, but mastering this concept is practically unavoidable for any developer. If you are interested in delving into caching, understanding what it is, how it works, and what are its most important types, you should follow this link first.
Getting Started
The Spring Boot Cache Abstraction does not come with the framework natively but requires a few dependencies. Thankfully, you can easily install all of them by adding the spring-boot-starter-cache
to your dependencies.
If you are a Gradle user, add this dependency to your build.gradle
file:
implementation "org.springframework.boot:spring-boot-starter-cache:2.5.0"
While if you are a Maven user, add the following dependency to your pom.xml
file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.5.0</version>
</dependency>
In detail, spring-boot-starter-cache
brings the spring-context-support
module, which transitively depends on spring-context
. The latter allows Spring to deal with contexts, also called Spring IoC (Inversion of Control) containers, which are responsible for managing objects in a Spring application. In particular, they are in charge of configuring, instantiating, and assembling Beans by reading configuration files and employing annotations. While the spring-context-support
module provides support for integrating third-party cache engines, which will be presented later, into a Spring application.
Follow this link from the Spring official documentation for further reading on the Spring IoC containers and the bean management.
Spring Boot Caching Annotations
After adding the dependencies required to start using the Spring Cache Abstraction mechanism, it is time to see how to implement the desired caching logic. This can easily be achieved by marking Java methods with specific caching-related annotations. Let's delve into the most important ones, showing where to use them and how.
@EnableCaching
To enable the Spring Boot caching feature, you need to add the @EnableCaching
annotation to any of your classes annotated with @Configuration
or to the boot application class annotated with @SpringBootApplication
.
@SpringBootApplication
@EnableCaching
public class SpringBootCachingApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootCachingApplication.class, args);
}
}
@EnableCaching
automatically sets up a valid instance of the CacheManager
interface, which is needed to enable caching. In particular, this annotation look for one of the many cache engines we will analyze later in the article. If not found, it creates a ConcurrentMapCacheManager
, which provides a default implementation of an in-memory cache based on a ConcurrentHashMap
object.
@Cacheable
This method-level annotation lets Spring Boot know that the return value of the annotated method can be cached. Each time a method marked with this @Cacheable
is called, the caching behavior will be applied. In particular, Spring Boot will check whether the method has been already invoked for the given arguments. This involves looking for a key, which is generated using the method parameters by default. If no value is found in the cache related to the method for the computed key, the target method will be executed normally. Otherwise, the cached value will be returned immediately.
@Cacheable
comes with many parameters, but the simplest way to use it is to annotate a method with the annotation and parameterize it with the name of the cache where the results are going to be stored.
@Cacheable("authors")
public List<Author> getAuthors(List<Int> ids) { ... }
You can also specify how the key that uniquely identifies each entry in the cache should be generated by harnessing the key attribute.
@Cacheable(value="book", key="#isbn")
public Book findBookByISBN(String isbn) { ... }
@Cacheable(value="books", key="#author.id")
public Books findBooksByAuthor(Author author) { ... }
Lastly, it is also possible to enable conditional caching as in the following example:
// caching only authors whose full name is less than 15 carachters
@Cacheable(value="authors", condition="#fullName.length < 15")
public Authors findAuthorsByFullName(String fullName) { ... }
@CachePut
This method-level annotation should be used when you want to update (put) the cache without avoiding the method from being executed. This means that the method will always be executed — according to the @CachePut
options — and its result will be stored in the cache. The main difference between @Cacheable
and @CachePut
is that the first might avoid executing the method, while the second will run the method and put its results in the cache, even if there is already an existing key associated with the given parameters. Since they have different behaviors, annotating the same method with both @CachePut
and @Cacheable
should be avoided.
@CacheEvict
This method-level annotation allows you to remove (evict) data previously stored in the cache.
By annotating a method with @CacheEvict
you can specify the removal of one or all values so that fresh values can be loaded into the cache again.
If you want to remove a specific value, you should pass the cache key as an argument to the annotation, as in the following example:
@CacheEvict(value="authors", key="#authorId")
public void evictSingleAuthor(Int authorId) { ... }
While if you want to clear an entire cache you can the parameter allEntries
in conjunction with the name of cache to be cleared:
@CacheEvict(value="authors", allEntries=true)
public String evictAllAuthorsCached() { ... }
This annotation is extremely important because size is the main problem of caches. A possible solution to this problem is to compress data before caching, as explained here. On the other hand, the best approach should be to avoid keeping data that you are not using too often in the caches. In fact, since caches can become large very quickly, you should update stale data with @CachePut
and remove unused data with @CacheEvict
. In particular, having a method to clean all the caches of your Spring Boot application easily can become essential. If you are interested in implementing an API aimed at this, you can follow this tutorial.
@CacheConfig
This class-level annotation allows you to specify some of the cache configurations in one place, so you do not have to repeat them multiple times:
@CacheConfig(cacheNames={"authors"})
public class AuthorDAO {
@Cacheable
publicList<Author> findAuthorsByFullName(String fullName) { ... }
@Cacheable
public List<Author> findAuthorsByBook(Book book) { ... }
// ...
}
Supported Cache Providers
As mentioned earlier, Spring Boot uses a ConcurrentMapCacheManager
object as the default cache engine, but it also provides integration with many third-party cache providers.
Let's have a brief look at the most important ones.
For more detailed information about the cache providers supported by Spring Boot, read this page from the official documentation.
Hazelcast
Hazelcast provides a cache-as-a-service for scalable, reliable, and fast caching. By using Hazelcast, developers can separate the caching layer from the application layer without worrying about the cache implementation.
Gradle dependency
implementation "com.hazelcast:hazelcast-all:4.0.2"
Maven dependency
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-all</artifactId>
<version>4.0.2</version>
</dependency>
EhCache
Ehcache provides an implementation of the JCache JSR-107 standard, and it is the most widely-used Java-based cache manager, well known for being robust, full-featured, and easy to integrate with popular frameworks.
Gradle dependencies
implementation "javax.cache:cache-api:1.1.1"
implementation "org.ehcache:ehcache:3.9.4"
Maven dependencies
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.4</version>
</dependency>
Infinispan
Infinispan is an in-memory data grid that offers features for storing, managing, and processing data. It provides a key/value data store that can hold all types of data and be used for caching.
Gradle dependency
Embedded Mode
implementation "org.infinispan:infinispan-spring-boot-starter-embedded:12.1.4.Final"
Remote Client/Server Mode
implementation "org.infinispan:infinispan-spring-boot-starter-remote:12.1.4.Final"
Maven dependency
Embedded Mode
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-spring-boot-starter-embedded</artifactId>
<version>12.1.4.Final</version>
</dependency>
Remote Client/Server Mode
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-spring-boot-starter-remote</artifactId>
<version>12.1.4.Final</version>
</dependency>
Redis
Redis is an in-memory key-value data structure store that can be used as a database, cache, and message broker.
Gradle dependency
implementation "org.springframework.boot:spring-boot-starter-data-redis:2.5.0"
Maven dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.0</version>
</dependency>
Caffeine
Caffeine is a high-performance, near-optimal caching library providing an in-memory cache using a Google Guava inspired API.
Gradle dependency
implementation "com.github.ben-manes.caffeine:caffeine:3.0.2"
Maven dependency
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.0.2</version>
</dependency>
Couchbase
Couchbase is a distributed NoSQL cloud database that also offers a fully integrated caching layer, providing high-speed data access.
Gradle dependency
implementation "com.couchbase.client:java-client:3.1.5"
Maven dependency
<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>java-client</artifactId>
<version>3.1.5</version>
</dependency>
Spring Boot Caching in Action
Let's see Spring Boot caching in action with an example. You can either clone the GitHub repository that supports this article or continue following this tutorial.
The demo project consists of a single REST service reachable from a GET request. Such an API will return a list of Author
objects, whose retrieval logic will be simulated thanks to a 3-second delay. When hit for the first time, the API will take more than 3 seconds to respond. On the contrary, on successive calls, the response will be almost instantaneous. This is the effect of the Spring Boot caching system, thanks to which the slow API logic will be avoided when it is possible to retrieve the desired data from a cache.
@Getter
@Setter
public class Author {
private static int id = 0;
private String name;
private String surname;
private String birthDate;
public Author() {}
public Author(
String name,
String surname,
String birthDate
) {
id++;
this.name = name;
this.surname = surname;
this.birthDate = birthDate;
}
}
The @Getter
and @Setter
annotations used in the code examples above are part of the Project Lombok. They are used to automatically generate getters and setters. This is not mandatory and just an additional way to avoid boilerplate code.
@Service
public class AuthorService {
@Cacheable("authors")
public List<Author> getAll() {
// simulating a delay due to the data retrieval operation
try {
System.out.println("Retrieving all the authors...");
Thread.sleep(3000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
// returning a list containing all the authors
return Arrays.asList(
new Author("Patricia", "Brown", null),
new Author("James", "Smith", "1964-07-01"),
new Author("Mary", "Williams", "1988-11-19")
);
}
}
Please, note that the retrieveAll()
service layer method is annotated with @Cacheable("authors")
. This way, the data returned from this method will be stored in a cache named "authors", as explained earlier.
@RestController
@RequiredArgsConstructor
@RequestMapping("/authors")
public class AuthorController {
private final AuthorService authorService;
@GetMapping
public ResponseEntity<List<Author>> getAll() {
loggingMessage("Request received!");
// retrieving the desired data
List<Author> authors = authorService.findAll();
loggingMessage("Data retrieved!");
return new ResponseEntity<>(
authors,
HttpStatus.OK
);
}
private void loggingMessage(
String message
) {
System.out.printf("[%s] %s%n", java.time.LocalTime.now().truncatedTo(ChronoUnit.MILLIS), message);
}
}
As you can see from the log below, the first time http://localhost:8080/authors
is hit, the response time is more than 3 seconds, and the data returned from retrieveAll
will be stored in the cache name authors. Then, when the endpoint is hit again, the desired data is returned immediately, and the method is not executed, as you notice from the missing data retrieving logging message.
[21:23:37.919] Request received!
Retrieving all the authors...
[21:23:40.990] Data retrieved!
[21:23:42.844] Request received!
[21:23:42.846] Data retrieved!
Conclusion
In this article, we looked at what you should know to start dealing with caching in a Spring Boot application. Caching has become increasingly important in computer science, and implementing a properly defined caching system is not an easy task. Luckily, Spring Boot comes with the Spring Cache Abstraction, which makes everything much easier. As shown, by employing a handful of annotations, you can implement your desired caching logic effortlessly. This would be impossible without using a cache engine, but as we have seen, you can choose your favorite from the many supported by Spring Boot. In conclusion, dealing with caching in Spring Boot is not as complex as you might expect. As a consequence, every Spring Boot developer should master its key aspects, and explaining them is what this article was aimed at.
Thanks for reading! I hope that you found this article helpful. Feel free to reach out to me with any questions, comments, or suggestions.