8 min read

Understanding Spring Boot Caching with an example

December 12, 2020

Caching is a concept that improves response time by storing copies of most frequently used data on a temporary but fast storage. In this article, We will see how to enable caching for a spring boot application with an example.

Imagine that you have a shopping website that has a number of product offerings. In ideal cases, There may be a lot of traffic for new orders/purchases. However, The information about the product details barely change. In these cases it would make sense not to load the price and product information from the database every single time. These are the situations where a cache store is helpful.Checkout this simple service that returns item details from the database.

@Service
public class ItemService {

    private final ItemRepository itemRepository;

    public ItemService(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    public List<Item> getItems() {
        return itemRepository.findAll();
    }

    public Item getItem(Integer id) {
        return itemRepository.findById(id).orElseThrow(RuntimeException::new);
    }

    public Item createItem(Item item) {
        return itemRepository.save(item);
    }

    public Item updateItem(Integer id, Item request) {
        Item item = getItem(id);
        item.setPrice(request.getPrice());
        item.setProductName(request.getProductName());
        return itemRepository.save(item);
    }

}

Simple caching implementation using java

The problem with the above code is that it will overload the database by keep loading the values from database on every call. This behaviour can be avoided if we introduce a simple caching mechanism by making few changes to our service.

@Service
public class ItemService {

    private static final Logger logger = LoggerFactory.getLogger(ItemService.class);

    private static final Map<Integer, Item> itemCacheMap = new HashMap<>();

    private final ItemRepository itemRepository;

    public ItemService(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    public List<Item> items() {
        return itemRepository.findAll();
    }

    public Item getItem(Integer id) {
        Item itemFromCache = itemCacheMap.get(id);
        if (itemFromCache != null) {
            logger.info("Loading data from Cache {}", itemFromCache);
            return itemFromCache;
        }
        Item item = itemRepository.findById(id).orElseThrow(RuntimeException::new);
        logger.info("Loading data from DB {}", item);
        itemCacheMap.put(id, item); //cache the value from DB
        return item;
    }

    public Item createItem(Item item) {
        return itemRepository.save(item);
    }

    public Item updateItem(Integer id, Item request) {
        Item item = getItem(id);
        item.setPrice(request.getPrice());
        item.setProductName(request.getProductName());
        itemCacheMap.remove(id); //invalidate cache
        return itemRepository.save(item);
    }

}

Spring Boot Cache implementation

Even though the above code get things done, It doesn’t look good. There is a better way to do this using spring-boot’s builtin cache support. In order to make use of this feature, We need to annotate the SpringBootApplication main class with @EnableCaching. this annotation makes sure that at-least a simple in-memory cache manager is autoconfigured. More on the cache managers later.

We can instruct spring boot’s cache mechanism using @Cacheable and @CacheEvict annotation on where and how to store and clear the cache. Take a look at the following code.

@Service
public class ItemService {
    private static final Logger logger = LoggerFactory.getLogger(ItemService.class);
    private final ItemRepository itemRepository;

    public ItemService(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    public List<Item> items() {
        return itemRepository.findAll();
    }

    @Cacheable(value = "items", key = "#id")
    public Item getItem(Integer id) {
        Item item = itemRepository.findById(id).orElseThrow(RuntimeException::new);
        logger.info("Loading data from DB {}", item);
        return item;
    }

    public Item createItem(Item item) {
        return itemRepository.save(item);
    }

    @CacheEvict(value = "items", key = "#id")
    public Item updateItem(Integer id, Item request) {
        Item item = getItem(id);
        item.setPrice(request.getPrice());
        item.setProductName(request.getProductName());
        return itemRepository.save(item);
    }
}

In the @Cacheable(value = "items", key = "#{id}") annotation, the value field takes a name. You can consider this as the replacement for the itemCacheMap.The key field is the integer id that we used to lookup itemCacheMap. Every time the getItem method is called, the returned Item object is stored in the items cache. The next call to this method with the same id will not lookup go into the method at all. Instead Spring Boot will return the value from cache. Let’s test this out ourselves.

first I have created few records for testing using CommandLineRunner. You don’t have to do this.

id productName price
1 Shirt Small 28.99
2 Pants Large 21.99

Now, Let’s hit the GET api at http://localhost:8080/items/2 for item with id 2. This returns with the following JSON.

{
  "id": 2,
  "productName": "Pants Large",
  "price": 21.99
}

In the logs you will also see that the application loaded this value from database. .

.ItemService:Loading data from DB Item{id=2,productName='Pants Large',price=21.99}

If you try hitting the same API one more time, You will observe that the same log is not printed anymore. This behaviour explains that the control of the method call didn’t pick the value from the database.

Now there is a problem. If I update the price of 2, the cache will still hold the old value right? In order to avoid situations like these, you need to instruct spring Boot when to clear the cached values. In this case on every call to updateItem method. This is where the @CacheEvict annotation comes in to the picture. You can also notice here that both eviction and storing of the cache are happening based on the id of the object that is being cached. This behaviour is governed by the key field of the annotations. This field takes a springEL expression as input.(More on this in upcoming sessions).

Here is an illustration of our current setup if we have multiple instances of the same application.

App 1
App 2
load from database
update cache with new data
cache doesn't contain data
get from cache
in-memory-cache of App 1
getItem
in-memory-cache of App 2
getItem
Client Request
Database

With this implementation we solved the problem of loading items from the database every time. Even though this implementation works, there are a couple of problems with this approach.

  1. These simple caches are in-memory within the application. Thus, they take a lot of heap memory which could lead to OutOfMemoryError. This is the reason why you don’t see me caching getItems() method. The result of this method is a List and, we may not know how big this list can be.
  2. Eviction is local to that specific instance’s in-memory cache storage. If you run multiple instances of the same application then update from one application will not reflect in the cache of other application. This will cause inconsistencies within the system.

Things to consider

There are a couple of things to note when using spring cache mechanisms.

all Component methods are @Cacheable.

@Cacheable can be applied to @Service,@Controller and even @Repository. As long as you can autowire a component, then their methods can be cacheable. I can very well do the below. However, It is better to keep the caching close to the business logic.

@Repository
public interface ItemRepository extends JpaRepository<Item, Integer> {

    @Cacheable(value = "items",key = "#id")
    Item findById(Integer id);
    
}

Only public methods are cacheable.

@Cacheable method has to be public. The annotation will simply be ignored for private and protected methods. This is due to

Cache won’t work within same class

Cache mechanism won’t work within the same bean. For example, The updateItem() method uses getItem(). If you look at the logs when you update the entries, You will see that the value was loaded from DB when getItem was called even if the data was cached already. This is due to how the underlying caching implementation works. Whenever a Component contains one or more @Cacheable methods, Spring will create a Wrapper/Proxy object around it and this proxy object is autowired everywhere. Methods within the same object doesn’t know about this proxy implementation at all and hence you can’t utilize the caching features within the same component.

Clearing entire cache

You can evict all cache at once using @CacheEvict(value="items",allEntries=true). This option is much effective when used along with @Scheduled annotation. These type of implementations may be helpful if the data changes at a specific duration.

As we discussed earlier, There are problems like cache inconsistency, OutOfMemoryErrors and caches not having an expiry time can be a pain for enterprise applications. This is where we need to move the cache store outside the applications. There are third-party solutions that provide high performance cache stores. In the upcoming article we will discuss more about Redis cache. One of the popular cache solutions right now.

Application Restarts will clear Cache

Because the cached objects are in heap memory, the cache is cleared as the heap memory is released after stopping the application. This one should be easier for you to test.

When to use caching?

There is only one golden rule for caching. That is to cache those values that barely change over time and takes longer time to compute. There is no point caching response of a method if the values are going to change on every call. This would be disastrous. Also, don’t cache results of smaller operations. A simple computation may be faster compared to loading the values from cache.