DEV Community

Cover image for Spring Boot: Conditional Caching in 6 lines of code.
Esteban Hernández
Esteban Hernández

Posted on

Spring Boot: Conditional Caching in 6 lines of code.

The Spring Boot framework includes a simple, server-side caching solution which is easy to enable and see immediate results and even easier to extend using robust caching solutions all through an Annotation based interface that makes it all feel like configuration rather than implementation.

When you work with different sources of data, your cache control rules may vary and conditions may need to be evaluated before performing caching operations. With this requirement, we risk polluting our business code with caching logic. Spring Boot's @Cacheable interface handles this elegantly by employing Java Annotations and the Spring Expression Language (SpEL).

Use Case

The Demo application serves as Backend-For-Frontend service for Typicode's JSONPlaceholder which serves RESTful JSON samples which can then be used for demos and tests concerning external APIs.

For the sake of this tutorial, the JSONPlaceholder API has a significant network latency of 3 seconds in every call to read collections and records. We could cache every single endpoint but the stakeholders have stated an additional requirement. We can only cache the /users/, /albums and /photos endpoints. All the others endpoints contain data that cannot be cached for reasons. The Front-End team will implement a loading screen for those endpoints instead.

Starter Template

To setup the demo, clone the Github repo and checkout the 1.0 tag as a new branch.

$ git clone https://github.com/LySofDev/java-spring-conditional-caching-demo.git .

$ cd java-spring-conditional-caching-demo

$ git checkout tags/1.0 -b demo
Enter fullscreen mode Exit fullscreen mode

Implementation Details

The PlaceholderController will pass along the arguments from the request URL and the application configuration to the JSONPlaceholder service which will perform the REST call to the JSONPlaceholder API.

src/main/java/com/example/conditionalcachingdemo/controllers/PlaceholderController.java

@GetMapping("/{name}")
public JSONArray getCollection(@PathVariable String name) {
    return placeholderService.getCollection(name);
}

@GetMapping("/{name}/{id}")
public JSONObject getRecord(@PathVariable String name, @PathVariable int id) {
    return placeholderService.getRecord(name, id);
}
Enter fullscreen mode Exit fullscreen mode

src/main/java/com/example/conditionalcachingdemo/services/PlaceholderServiceImpl.java

@Override
public JSONArray getCollection(String name) {
    try {
        JSONArray collection = restTemplate.getForObject(collectionUrl(name), JSONArray.class);
        Thread.sleep(3000); // Simulating a slow network.
        return collection;
    } catch (HttpClientErrorException.NotFound | InterruptedException e) {
        throw new CollectionNotFoundException();
    }
}

@Override
public JSONObject getRecord(String name, int id) {
    try {
        JSONObject record = restTemplate.getForObject(recordUrl(name, id), JSONObject.class);
        Thread.sleep(3000); // Simulating a slow network.
        return record;
    } catch (HttpClientErrorException.NotFound | InterruptedException e) {
        throw new RecordNotFoundException();
    }
}
Enter fullscreen mode Exit fullscreen mode

These two code excerpts cover the meat of the application. Everything else in the repository is built to provide this functionality to the end user. Feel free to take a moment to look at the rest of the repo as most files are brief.

To simulate the JSONPlaceholder API's network latency, we'll include a 3 second wait period in our code after fetching data from the API.

Enabling the Cache

src/main/java/com/example/conditionalcachingdemo/ConditionalCachingDemoApplication.java

@SpringBootApplication
@EnableCaching
public class ConditionalCachingDemoApplication {
Enter fullscreen mode Exit fullscreen mode

The implementation of the cache is based on the CacheManager interface. The Spring framework includes a simple implementation which stores cache data in memory. This is fine for development but, in production, we'll want to use an external caching solution like Redis or Cassandra. Replacing the default implementation is as simple as providing an alternative implementation of the CacheManager interface.

Cache Configuration

We'll update the application configuration to include a list of the cacheable endpoints.

src/main/resources/application.properties

api.url=https://jsonplaceholder.typicode.com/
api.cacheables=users,albums,photos
Enter fullscreen mode Exit fullscreen mode

Then, we'll add the @Cacheable annotations to the PlaceholderController and update the method signatures to include the api.cacheables configuration values.

src/main/java/com/example/conditionalcachingdemo/controllers/PlaceholderController.java

@GetMapping("/{name}")
@Cacheable(value = "collections", condition = "#cacheables.contains(#name)")
public JSONArray getCollection(@PathVariable String name, @Value("${api.cacheables}") List<String> cacheables) {
    return placeholderService.getCollection(name);
}

@GetMapping("/{name}/{id}")
@Cacheable(value = "records", key = "#name + #id", condition = "#cacheables.contains(#name)")
public JSONObject getRecord(@PathVariable String name, @PathVariable int id, @Value("${api.cacheables}") List<String> cacheables) {
    return placeholderService.getRecord(name, id);
}
Enter fullscreen mode Exit fullscreen mode

That's it. Run the Spring Boot application and hit the cacheable endpoints. The first time we hit an endpoint, it will take 3 seconds to respond since the cache hasn't yet been hydrated. Once the data from JSONPlaceholder is loaded into the cache, subsequent requests to the endpoint will be served almost immediately since the data is coming from memory and not the network.

Solution Analysis

The @Cacheable annotation includes two arguments in the getCollection method. The value argument indicates the cache name for the collection. This allows us to set different configurations per cache such as expiry times, etc. The condition argument validates that the name of the endpoint provided in the URL is included in the api.cacheables configuration.

You might be wondering why we include the cacheables value in the controller method signature instead of making it part of the class itself. The SpEL only interacts with the variables that are declared static but the @Value annotation can only be applied on an instance variable or a method parameter. Therefore, to provide the configuration values api.cacheables to the @Cacheable condition, we'll include them as part of the method signature List<String> cacheables and let the @Value annotation fill the values in during runtime.

In the getRecord method, we also use the key argument to add a mapping expression #name + #id to generate the key for this record in cache from the collection name and record id. In the getCollections method, the key will default to the value of the name variable.

Conclusion

The Spring Boot application now satisfies the caching requirement and can be pushed to a UAT environment for demonstration to the stakeholders while the underlying caching solution for the production environment is developed.

Top comments (0)