Implementing LoadingCache in Java Spring Boot using Guava
This article demonstrates an effective technique to implement LoadingCache in Java Spring Boot Service using Google Guava Cache API.
Background
In my previous article, I have explained the importance of using the Cache and then demonstrated a way to implement Guava Cache in the Java Spring Boot service.
In that example, we first checked whether the Record exists in Cache. If the Record is found in Cache, we skipped the backend service call. Otherwise, we fetched the Record details using the backend service and stored them in Cache for future use. That seems like a lot of manual handling. Can this be simpler?
Guava does provide another easy way for Cache management thru LoadingCache. A LoadingCache is a Cache built with an attached CacheLoader. We configure CacheLoader by pointing it to the backend service, which can then be used to load values using a KEY when it is not already present in Cache.
By managing retrieval and storing of records by itself, the LoadingCache makes it easier to implement the entire Caching mechanism. We only need to call method get(K), and it will either return an already cached value or atomically load a new value into the Cache using the Cache’s CacheLoader.
Implementing LoadingCache Example
Let’s see how we can implement a Cache Store with the help of Guava LoadingCache.
The rest of the article details all the steps we need to implement LoadingCache. If it is helpful, you may also download the final source code from GitHub Link — Implementing Guava LoadingCache.
Note: Steps #1 thru #3 are the same as listed in my previous article.
Step 1: Create a Spring Boot Project
We need one Spring Boot project. The basic skeleton can be created quickly by using https://start.spring.io/ as below,
Step 2: Write HTTP GET REST API
Write one sample REST API as detailed below. In this API, we are simulating a backend service call by adding an intentional wait time. The next step will introduce the Guava LoadingCache to cache the API response and improve the overall latency.
When we build and run this application, it will produce the below output, which shows some latency as each time call is sent to backend service to fetch data,
Step 3: Add Guava Dependency
In build.gradle file add Guava library to the list of dependencies as,
implementation 'com.google.guava:guava:latest.release'
Step 4: Define a Class to provide LoadingCache Store for any generic type <T>
Let’s call this class LoadingCacheStore, which will help us to build LoadingCache for any generic type. The LoadingCache is configured with a CacheLoader, which internally uses a backend service to load values using cache KEY when it is not already present in the Cache.
Please Note:
- The use of interface ICacheLoaderService provides a way to abstract underlying backend service by forcing them to implement one shared method: public T getBackendData(key). That enables reusability for Class LoadingCacheStore to implement a caching mechanism for multiple backend APIs when required.
- Configured CacheLoader makes implementation much leaner by removing the need to explicitly storing KEY-Value pairs in the Cache.
- As illustrated later in Step #6: using LoadingCacheStore<T>, we can create multiple Beans to implement Cache for as many Types as we need.
- expireAfterWrite() provides the ability to invalidate the Cache Records accordingly to the supplied parameters.
Step 5: Add implementation to EmployeeService and implement the inherited method.
The use of interface ICacheLoaderService provides a way to abstract underlying backend service by forcing them to implement the inherited method: public T getBackendData(key).
Step 6: Define a Java Bean to generate a new LoadingCache Store of record type Employee.
Step 7: Modify ApiController Class to implement the use of LoadingCache for Employee records.
Here in this step, we will use the LoadingCacheStore<Employee> instance, as if we are directly consuming the backend API.
The LoadingCacheStore<Employee> will internally manage all steps required for Cache implementation like,
- skipping backend API call when the Record is present in Cache,
- calling backend API when the Record is missing in Cache,
- storing the Record in Cache for future use,
- and invalidating or removing the stored Record as per cache-retention configurations.
Please Note:
- For the first request, the Employee record is not present in Cache. Hence LoadingCacheStore<Employee> will call backend API to fetch the Employee record. (Here, the backend API has an intentional delay to simulate an HTTP call over the network.)
- The fetched Employee record will then be stored first in the Cache instance before returning it to the consumer.
- For subsequent requests, LoadingCacheStore<Employee> will find the Employee record in the Cache. LoadingCache will immediately return the cached Employee record in the response by skipping the backend API call.
- In Step #6, we had created Cache-Store with an expiry duration of 120 seconds. Therefore, the LoadingCache will retain the Employee record only for 120 Seconds. Guava Cache will keep track of time and invalidate/remove the Employee record from Cache by itself post 120 seconds.
Step 8: Demo
Build and Run Api Application on local. To test Caching go to URL:
http://localhost:8080/employee/1
As this will be the first call, employee key “1” will not be found in Cache. Therefore, LoadingCache will invoke a backend service call, and we will see some latency in response.
Test the same URL a second time, and you will see improved response time as the record from Cache is returned by skipping the backend service call.
Next, test for another employee id as:
http://localhost:8080/employee/2
This new employee key “2” is not present in Cache. Hence LoadingCache will make a backend service call to fetch details.
Refer to sequential logs below for all the 3 tests:
Step 9: Reusing LoadingCacheStore<T> class to create another LoadingCache of a different record type.
Once we follow Steps #1 thru #8, we will have provision to quickly add a new Cache-Store of any number of record types.
Refer to the below example to know what all changes needed to generate any new Cache-Store:
Please Note: Here, Product.java and ProductService.Java follow the same pattern as previously provided Employee.Java and EmployeeService.Java. If you are interested, you may download the complete source code from GitHub Link — Implementing Guava LoadingCache.
9a. Add ICacheLoaderService<T> Implementation for any backend API you wish to implement Caching functionality.
Please note: getBackendData(KEY) method here is fetching a sizable record from backend API but returning only a truncated one as required for our Cache. That avoids unnecessary overloading of application server memory.
9b. Define a Bean for new LoadingCache using the required backend API interface: e.g., LoadingCacheStore<String>.
9c. Use newly defined LoadingCacheStore<String> productNameLoadingCache to fetch values.
Summary
In this article, we have seen an effective technique to implement Guava LoadingCache in Spring Boot. LoadingCache abstracts most Cache management features like retrieval, loading, and invalidating, making implementation a lot easier from the Cache consumer perspective.
Download the complete source code for this article from GitHub Link — Implementing Guava LoadingCache.
Happy Coding!