Caching in Your Spring Boot Application

One of the features of the Spring Framework is a cache you can transparently add to your code. All you need to do is set an annotation on a method you want to cache and configure the caching mechanism itself. Setting up the cache wasn't a big deal before, and with Spring Boot it got even simpler. Let's dive into it with an example.

The Example

The example is a simple web service with two endpoints. One retrieves a product by id, and the other can update the product. We simulate the storage with a variable identifier but an otherwise fixed product.

The source code for this tutorial is available on GitHub, but let's do a quick walkthrough.

The application:

@SpringBootApplication
public class CachingTutorialApplication {

    public static void main(String[] args) {
        SpringApplication.run(CachingTutorialApplication.class, args);
    }
}

The model:

public class Product implements Serializable {

    private String id;
    private String ean;
    private String name;

    //constructors and getter, setters omitted

}

The service:

@Service
public class ProductService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ProductService.class);

    public Product getProduct(String id) {
        LOGGER.info("getProduct called for id {}", id);
        return new Product(id, "0826663141405", "The Angry Beavers: The Complete Series");
    }

    public void updateProduct(Product product) {
        LOGGER.info("updateProduct called for id {}", product.getId());
        //do nothing, we just simulate the update
    }
}

And last but not least, the controller:

@RestController
public class ProductController {
    private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class);

    @Autowired
    private ProductService productService;

    @GetMapping("product/{id}")
    public Product getProduct(@PathVariable("id") String id) {
        LOGGER.info("getProduct called for id {}", id);
        return productService.getProduct(id);
    }

    @PostMapping("product/{id}")
    public Product updateProduct(@PathVariable("id") String id, @RequestBody Product product) {
        LOGGER.info("updateProduct called for id {}", id);
        product.setId(id);
        productService.updateProduct(product);
        return productService.getProduct(id);
    }
}

With a GET to /product/{id} we retrieve a product as JSON and with a POST to the same URL, we change the product.

The service and controller classes do log, so we can follow on the console that the cache works in the next steps. If we retrieve the product now twice, without the cache, the log output will look like:

2017-09-20 10:59:46,461 - ProductController    : getProduct called for id 4711
2017-09-20 10:59:46,465 - ProductService        : getProduct called for id 4711
2017-09-20 10:59:53,767 - ProductController    : getProduct called for id 4711
2017-09-20 10:59:53,768 - ProductService        : getProduct called for id 4711

Every time, getProduct is called on the controller it is also called on the service.

The Cache

The cache mechanism in Spring is abstract and not bound to a particular implementation. We can choose between several backends, e.g., in-memory, Redis, Eh-Cache and more. In the center of it are the CacheManager and Cache interfaces. Cache is an interface for working with a concrete cache instance. And CacheManager is used for obtaining and working with the _Cache_s. Both are transparent for the application, and we must declare and setup once which cache provider we are using.

To get started, we add the cache starter to our dependencies.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Next, we must enable the caching feature for our application by adding the @EnableCaching annotation to a @Configuration like:

@SpringBootApplication
@EnableCaching
public class CachingTutorialApplication {

    public static void main(String[] args) {
        SpringApplication.run(CachingTutorialApplication.class, args);
    }
}

The auto-configuration will now enable caching for us and will set up a CacheManager for us if it does not find one. It will first scan in a particular order for certain providers, and when it does not find one, it will create a simple in-memory cache using concurrent maps.

In the first variation, we will use the in-memory cache.

Now that we have a working cache backend, we can start to use it. For that, Spring Caches offers a few annotations. We will focus on the most commonly used.

  • @Cacheable: Defines a cache for a methods' return value
  • @CacheEvict: When the method executes, we empty the whole cache or a single entry

All annotations are used on methods.

Define a Cache

We are using the cache in the ProductService so we can see the effect of the cache better in the console. However, we could also use the cache annotations on the controller methods.

To create a cache for the getProduct method we add @Cacheable to it like:

@Cacheable("product")
public Product getProduct(String id) {
    LOGGER.info("getProduct called for id {}", id);
    return new Product(id, "0826663141405", "The Angry Beavers: The Complete Series");
}

We can give the cache a name either by using the value parameter shortcut or cacheNames. It is possible to assign the object to multiple caches at once by just declaring their names in these parameters.

Each entry in the cache is identified by a unique key. As we have not specified a key in the @Cacheable annotation above, it will use a default mechanism to create the key.

When the method has:

  • no parameter, it uses a SimpleKey.EMPTY
  • one parameter it will use that instance
  • more than one parameter, it will build a joined key of the given parameters

The parameter types should implement valid hashCode() and equals() methods. As we use a String here, we are saved.

We can also specify the key explicitly by using the key parameter of the annotation. We will do that on the cache clear.

When we get the product now a few time, we'll receive the cached version aftter the first call. On the console it looks like:

2017-09-20 11:58:48,050 - ProductController    : getProduct called for id 4711
2017-09-20 11:58:48,066 - ProductService        : getProduct called for id 4711
2017-09-20 11:58:49,378 - ProductController    : getProduct called for id 4711
2017-09-20 11:58:50,399 - ProductController    : getProduct called for id 4711

What happens internally now, is the Spring detects when our service method is called, that it should put the result in a cache. It will use the id as the key value for our product and place it into the product cache accordingly. If the cache does not exist to this point, it will also create it for use (thanks, auto-magic).

With our current in-memory cache the product is cached as long as the application lives. So, if we would change our product in a database, which we simulate here, it won't be updated in the cache, as the implementation of our getProduct method isn't called anymore.

Delete Cache Entries

In this step, we are going to delete entries from the cache when we update our product.

To do so, we add the @CacheEvict annotation to our updateProduct method like:

@CacheEvict(cacheNames="product")
public void updateProduct(Product product) {
    LOGGER.info("updateProduct called for id {}", product.getId());
    //do nothing, we just simulate the update
}

When we only declare the cache to evict, it will clear the whole cache everytime our method is invoked. It is a valid approach, but a bit too much in our case. So, let's just delete the updated record.

@CacheEvict(cacheNames="product", key ="#root.args[0].id")
public void updateProduct(Product product) {
    LOGGER.info("updateProduct called for id {}", product.getId());
    //do nothing, we just simulate the update
}

The key parameter defines the key to use for the cache based on the method parameters. It is the same mechanism as in the @Cacheable above. In the key value, the Spring Expression Language (SpEL) is used, and we can use it to construct the cache key. The method name, return type, parameters and such are made available in the SpEL under the root variable. For example, with #root.methodName we could retrieve the method name, with #root.args the methods' parameters.

In our case, we use the id of the product as our cache key. As we only pass in a Product in the update method, we must access the actual id by #root.args[0].id. Spring will now evaluate this to take the first argument of the method and get the field id of it and use this as the key.

When you update the product now, Spring will only delete our product entry from the cache. On the next getProduct request, it will load the new value and put it into the cache again.

On the console it will look like:

2017-09-20 11:58:50,399 - ProductController    : getProduct called for id 4711
2017-09-20 12:18:28,853 - ProductController    : getProduct called for id 4711
2017-09-20 12:18:32,384 - ProductController    : updateProduct called for id 4711
2017-09-20 12:18:32,386 - ProductService        : updateProduct called for id 4711
2017-09-20 12:18:32,455 - ProductService        : getProduct called for id 4711
2017-09-20 12:18:35,719 - ProductController    : getProduct called for id 4711

Two calls of getProduct but the product is already in the cache. One call to updateProduct which will update the product and evict it from the cache. As the updateProduct in the controller will also call _getProduct on the service and return it to JSON, it is already cached with this request. In the following get calls on the controller, it comes out of the cache again.

Using Redis

The simple cache using a concurrent map is great, but let's switch to a better and more common way like Redis.

For using Redis as a cache provider, we only add the Spring Data Redis starter to our pom:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

The auto-configuration will now set up the Spring infrastructure for working with Redis and also as a cache provider. By default, it will use a Redis instance on localhost; which is fine for testing.

The Redis cache is higher in the search order for a cache provider than the concurrent map, so it will automatically get picked up.

To use a different instance, configure it in application.properties like:

spring.redis.host=192.168.0.100
spring.redis.port=6379

When we start the application now, it will use the Redis instance as a cache.

Cache Metrics

The caching feature does also provide some new metrics when we also use the Spring Boot Actuators in our application.

Namely:

  • Current size of the cache: cache..size
  • A hit ratio: cache..hit.ratio
  • A miss ratio: cache..miss.ratio

Be aware that not all cache providers provide the above metrics.

Conclusion

Adding a transparent cache in your Spring Boot application is done in a breeze. Most of the time, it will work with the basics we covered above. And for the case it doesn't, the feature is powerful enough to support more complex situations.