Implementing Cache using Caffeine in a Reactive App that uses Spring WebFlux and MongoDB
Step-by-step guide on how to implement cache in Book API using Caffeine
In this article, we will explain how to implement cache using Caffeine in a Spring Boot Reactive application, whose name is Book API.
Caffeine is a high-performance Java caching library that efficiently stores and manages data, automatically removing entries to control memory usage. It offers features like automatic loading, size-based eviction, time-based expiration, and various options for key and value references.
You can find the complete code and implementation in the article linked below. Feel free to follow the steps explained in the article and get started.
We’re implementing caching at the Book API controller level to achieve the following main goals:
- Faster Response Times: Caching enables quicker responses to user requests. Cached data, readily available in memory, is served much faster than retrieving data from a database, which involves disk I/O and potentially complex queries.
- Reduced Database Load: By lowering the database load, we maintain database performance and prevent it from becoming a bottleneck, especially during high-traffic periods.
- Lower Latency: Cached data is served with lower latency, which is crucial for real-time or near-real-time applications like gaming, financial services, or live event tracking.
- Improved User Experience: Faster response times and lower latency enhance the user experience, resulting in quicker load times and smoother interactions with your API, leading to higher user satisfaction.
Let’s begin implementing these improvements!
Updating Book API
Modify the pom.xml
In the pom.xml file, let’s include the Spring Cache Abstraction and Caffeine dependencies by adding the following content (highlighted in bold):
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
...
<dependencies>
...
<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>
...
</dependencies>
...
</project>Create the config package
In order to keep our code organized, let’s create the package config inside com.example.bookapi root package.
Create the CachingConfig class
In config package, let’s create the CachingConfig class with the content below:
package com.example.bookapi.config;
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 CachingConfig {
public static final String BOOKS = "BOOKS";
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setAsyncCacheMode(true);
return cacheManager;
}
}The CachingConfig class enables caching support for the application. It does this by turning on caching using the @EnableCaching annotation and telling the system that it's a configuration class with the @Configuration annotation. Additionally, it defines a constant string called BOOKS, which is used as the name for caching book-related data in the application.
It uses Caffeine as the caching provider and configures a cache manager. The cache manager is set to operate in asynchronous mode, which means that certain cache operations may be performed asynchronously for improved performance.
Update the application.properties
Let’s add in application.properties file, the following lines in bold:
spring.application.name=book-api
spring.data.mongodb.uri=mongodb://${MONGODB_HOST:localhost}:${MONGODB_PORT:27017}/bookdb
spring.cache.type=caffeine
spring.cache.caffeine.spec=initialCapacity=100, maximumSize=1000, expireAfterAccess=1h, recordStats
logging.level.org.springframework.data.mongodb.core=DEBUG
logging.level.org.springframework.cache=TRACEExplaining briefly the new properties:
spring.cache.type: This property sets the caching provider to Caffeine.spring.cache.caffeine.spec: This property configures Caffeine with specific settings:
initialCapacity=100: Starts with a cache size of 100 entries.maximumSize=1000: Limits the cache to 1000 entries.expireAfterAccess=1h: Cached data expires if not accessed for 1 hour.recordStats: Collects cache usage statistics.
3. logging.level.org.springframework.cache: Configures the logging level for Spring's caching mechanisms.
Update the BookController class
Let’s update the BookController class by adding the caching annotations to the endpoints (lines in bold):
package com.example.bookapi.controller;
...
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
...
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/books")
public class BookController {
...
@GetMapping(produces = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<BookResponse> getBook() {
...
}
@Cacheable(cacheNames = CachingConfig.BOOKS, key = "#id")
@GetMapping("/{id}")
public Mono<BookResponse> getBook(@PathVariable String id) {
...
}
@CachePut(cacheNames = CachingConfig.BOOKS, key = "#result.id")
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public Mono<BookResponse> createBook(@Valid @RequestBody CreateBookRequest createBookRequest) {
...
}
@CachePut(cacheNames = CachingConfig.BOOKS, key = "#id")
@PatchMapping("/{id}")
public Mono<BookResponse> updateBook(@PathVariable String id,
@RequestBody UpdateBookRequest updateBookRequest) {
...
}
@CacheEvict(cacheNames = CachingConfig.BOOKS, key = "#id")
@DeleteMapping("/{id}")
public Mono<BookResponse> deleteBook(@PathVariable String id) {
...
}
}1. Cacheable Annotation
@Cacheable is used on methods to indicate that the result of the method should be cached.
In this class, @Cacheable(cacheNames = CachingConfig.BOOKS, key = "#id") is applied to the getBook method.
cacheNamesspecifies the name of the cache in which the method's results will be stored. Here, it uses the cache name defined inCachingConfigclass, which isBOOKS;keyis used to specify the cache key. In this case, it uses#idas a SpEL (Spring Expression Language) expression to generate a dynamic cache key based on theidparameter of the method.
When a client calls this method with a specific id, Spring will check if the result for that id is already cached. If it's cached, Spring will return the cached result; otherwise, it will execute the method and cache the result for future requests.
2. CachePut Annotation
@CachePut is used to update the cache with the result of a method.
In this class, @CachePut(cacheNames = CachingConfig.BOOKS, key = "#result.id") is applied to the createBook and updateBook methods.
Similar to @Cacheable, it specifies the cache name and key. In the case of createBook, it uses #result.id as the key, which means it uses the id of the result to determine the cache key.
When these methods are called, they not only execute their logic but also update the cache with the result. This ensures that subsequent requests for the same data can be served from the cache.
3. CacheEvict Annotation
@CacheEvict is used to remove data from the cache.
In this class, @CacheEvict(cacheNames = CachingConfig.BOOKS, key = "#id") is applied to the deleteBook method.
It specifies the cache name and key. When this method is called (e.g., when deleting a book), it will remove the corresponding data from the cache based on the id provided.
Starting Environment
Start MongoDB
In a terminal, run the following command to start the MongoDB Docker container:
docker run -d --name mongodb -p 27017:27017 mongo:7.0.4
Start Book API
Make sure you are inside the book-api root folder. Then, run the command below to start Book API:
./mvnw clean spring-boot:run
Testing Endpoints
To verify the functionality of caching, we’ll initiate requests to the Book API endpoints from a terminal. Additionally, we’ll inspect the Book API logs to determine whether the requests are accessing the database or utilizing the cache.
Get all books
Let’s start by running the following cURL command to retrieve all books:
curl -i localhost:8080/api/books
As there is no book registered, we should get:
HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: application/x-ndjson
This endpoint doesn’t employ caching, as we haven’t annotated it with any caching annotation. By examining the Book API logs, we can see that it only contains the query to fetch all books.
find using query: {} fields: Document{{}} for class: class com.example.bookapi.model.Book in collection: booksCreate a book
Let’s create a book by submitting the following command:
curl -i -X POST localhost:8080/api/books \
-H 'Content-Type: application/json' \
-d '{"title": "Spring in Action", "author": "Craig Walls and Ryan Breidenbach", "year": 1005}'It should return:
HTTP/1.1 201 Created
Content-Type: application/json
...
{"id":"65785366a4b8da5ca5952a50","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":1005}Checking the Book API logs, we have:
Inserting Document containing fields: [title, author, year, _class] in collection: books
Computed cache key '65785366a4b8da5ca5952a50' for operation Builder[public reactor.core.publisher.Mono com.example.bookapi.controller.BookController.createBook(com.example.bookapi.controller.dto.CreateBookRequest)] caches=[BOOKS] | key='#result.id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless=''
Creating cache entry for key '65785366a4b8da5ca5952a50' in cache(s) [BOOKS]The application starts by inserting a new book into the “books” collection. Then, it stores the Mono<BookResponse> value, obtained from the book creation, under the key 65785366a4b8da5ca5952a50, placing this key-value pair into the Caffeine cache named BOOKS.
Note: In my case, the book ID is
65785366a4b8da5ca5952a50. For you, it will be a different value. In order to facilitate the writing of this article, let’s export the book ID to an environment variable. For it, run the following command in the terminal where you are performing the calls to Book API endpoints:
BOOK_ID=<the-created-book-id>
Retrieve the book created
Let’s now try to retrieve the book created:
curl -i localhost:8080/api/books/$BOOK_IDWe should get:
HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"65785366a4b8da5ca5952a50","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":1005}This request generated the logs below:
Computed cache key '65785366a4b8da5ca5952a50' for operation Builder[public reactor.core.publisher.Mono com.example.bookapi.controller.BookController.getBook(java.lang.String)] caches=[BOOKS] | key='#id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
Cache entry for key '65785366a4b8da5ca5952a50' found in cache(s) [BOOKS]As you can see, the application is checking the BOOKS cache to find an entry with the key 65785366a4b8da5ca5952a50. It finds and returns the Mono<BookResponse> value that was saved earlier when the book was inserted, and it does this without having to go to the database.
Update the book
Let’s now update the book created as its year is wrong! It should be 2005 instead of 1005:
curl -i -X PATCH localhost:8080/api/books/$BOOK_ID \
-H 'Content-Type: application/json' \
-d '{"year": 2005}'We should get:
HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"65785366a4b8da5ca5952a50","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}This request generated the following logs:
findOne using query: { "id" : "65785366a4b8da5ca5952a50"} fields: Document{{}} for class: class com.example.bookapi.model.Book in collection: books
Saving Document containing fields: [_id, title, author, year, _class]
Computed cache key '65785366a4b8da5ca5952a50' for operation Builder[public reactor.core.publisher.Mono com.example.bookapi.controller.BookController.updateBook(java.lang.String,com.example.bookapi.controller.dto.UpdateBookRequest)] caches=[BOOKS] | key='#id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless=''
Creating cache entry for key '65785366a4b8da5ca5952a50' in cache(s) [BOOKS]At the start, the application logs its database interactions. It begins by searching for a book with an ID of 65785366a4b8da5ca5952a50 and then proceeds to update the book record.
In the final step, it modifies the Mono<BookResponse> value associated with the key 65785366a4b8da5ca5952a50 within the Caffeine cache named BOOKS.
Retrieve the book updated
Let’s now try to retrieve the book updated:
curl -i localhost:8080/api/books/$BOOK_IDThe response should be:
HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"65785366a4b8da5ca5952a50","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}Now, the year is correct. The generated logs are:
Computed cache key '65785366a4b8da5ca5952a50' for operation Builder[public reactor.core.publisher.Mono com.example.bookapi.controller.BookController.getBook(java.lang.String)] caches=[BOOKS] | key='#id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
Cache entry for key '65785366a4b8da5ca5952a50' found in cache(s) [BOOKS]As observed, the application checks the BOOKS cache for a cached entry associated with the key 65785366a4b8da5ca5952a50. It successfully locates and retrieves the Mono<BookResponse>, which has been updated to reflect changes made to the book, all without needing to access the database.
Delete the book
Finally, let’s delete the book:
curl -i -X DELETE localhost:8080/api/books/$BOOK_IDThe response should be:
HTTP/1.1 200 OK
Content-Type: application/json
...
{"id":"65785366a4b8da5ca5952a50","title":"Spring in Action","author":"Craig Walls and Ryan Breidenbach","year":2005}Checking the generated logs:
findOne using query: { "id" : "65785366a4b8da5ca5952a50"} fields: Document{{}} for class: class com.example.bookapi.model.Book in collection: books
Remove using query: { "_id" : { "$oid" : "65785366a4b8da5ca5952a50"}} in collection: books.
Computed cache key '65785366a4b8da5ca5952a50' for operation Builder[public reactor.core.publisher.Mono com.example.bookapi.controller.BookController.deleteBook(java.lang.String)] caches=[BOOKS] | key='#id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='',false,false
Invalidating cache key [65785366a4b8da5ca5952a50] for operation Builder[public reactor.core.publisher.Mono com.example.bookapi.controller.BookController.deleteBook(java.lang.String)] caches=[BOOKS] | key='#id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='',false,false on method public reactor.core.publisher.Mono com.example.bookapi.controller.BookController.deleteBook(java.lang.String)Initially, the application logs its database interaction. It starts by searching for a book with an ID of 65785366a4b8da5ca5952a50 and subsequently remove the book.
Following the removal, it deletes the key 65785366a4b8da5ca5952a50 from the BOOKS cache, effectively invalidating it.
Retrieve the book deleted
If we try to retrieve the book deleted:
curl -i localhost:8080/api/books/$BOOK_IDThe following should be returned:
HTTP/1.1 404 Not Found
Content-Type: application/json
...
{"timestamp":"2023-12-12T12:39:50.339+00:00","path":"/api/books/65785366a4b8da5ca5952a50","status":404,"error":"Not Found","requestId":"a46e4238-7"}Checking the generated logs:
Computed cache key '65785366a4b8da5ca5952a50' for operation Builder[public reactor.core.publisher.Mono com.example.bookapi.controller.BookController.getBook(java.lang.String)] caches=[BOOKS] | key='#id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
No cache entry for key '65785366a4b8da5ca5952a50' in cache(s) [BOOKS]
findOne using query: { "id" : "65785366a4b8da5ca5952a50"} fields: Document{{}} for class: class com.example.bookapi.model.Book in collection: booksThe application attempts to locate an entry for the key 65785366a4b8da5ca5952a50 in the cache, but doesn’t find anything. Consequently, it queries the database. However, since the book has already been deleted, the application returns a “404 Not Found” response.
Shutdown
In the terminal where the Book API is running, press Ctrl+C to stop the application.
To stop the MongoDB Docker container, run the following command in a terminal:
docker rm -fv mongodbConclusion
In this article, we’ve implemented caching with Caffeine in a Spring Boot Reactive application named Book API. This caching is specifically applied at the Book API controller level, aiming to accomplish several key benefits, including faster response times, decreased database workload, reduced latency, and an overall improved user experience. Towards the end, we validate the application’s endpoints to ensure that incoming requests are efficiently directed to either the cache or the database, as appropriate.
Additional Readings
Support and Engagement
If you enjoyed this article and would like to show your support, please consider taking the following actions:
- 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
- 🌐 Share my story on Social Media;
- 🔔 Follow me on: Medium | LinkedIn | Twitter | GitHub;
- ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.





