Implementing a Cache with Spring Boot

Table Of Contents

We use a cache to protect the database or to avoid cost-intensive calculations. Spring provides an abstraction layer for implementing a cache. This article shows how to use this abstraction support with Hazelcast as a cache provider.

Example Code

This article is accompanied by a working code example on GitHub.

Why Do We Need a Cache Abstraction?

If we want to build a Spring Boot application and use a cache, usually we want to execute some typical operations like

  • putting data into the cache,
  • reading data from the cache,
  • updating data in the cache,
  • deleting data from the cache.

We have a lot of technologies available to set up a cache in our application. Each of these technologies, like Hazelcast or Redis, for example, has its own API. If we want to use it in our application, we would have a hard dependency on one of those cache providers.

The Spring cache abstraction gives us the possibility to use an abstract API to access the cache. Our business code can use this abstraction level only, without calling the Cache provider’s code directly. Spring provides an easy-to-use annotation-based method to implement caching.

Behind the abstraction, we can choose a dedicated cache provider, but the business logic doesn’t need to know anything about the provider.

The Spring abstraction layer lets us use a cache independently of the cache provider.

Cache Providers

Spring Boot supports several cache providers. If Spring Boot finds a cache provider on the classpath, it tries to find a default configuration for this provider. If it doesn’t find a provider, it configures the Simple provider, which is just a ConcurrentHashMap.

Enabling Spring’s Cache Abstraction with @EnableCaching

Let’s have a look at how to enable caching in a Spring Boot application.

First, we have to add a dependency to the cache starter (Gradle notation):

implementation 'org.springframework.boot:spring-boot-starter-cache'

This starter provides all classes we need to support the cache. These are mainly the interfaces Cache and CacheManager that should be implemented by the provider, and the annotations for the methods and classes that we can use to mark methods as cacheable.

Second, we need to enable the cache:

@Configuration
@EnableCaching
class EmbeddedCacheConfig {
 
  // Other methods omitted.

}

The annotation @EnableCaching will start the search for a CacheManger bean to configure the cache provider. After enabling the cache we are ready to use it. But we didn’t define any cache provider, so as mentioned above a Simple in-memory provider would be used. This simple cache might be good for testing, but we want to use a “real” cache in production.

We need a provider that supports several data structures, a distributed cache, a time-to-live configuration, and so on. Let’s use Hazelcast as a cache provider. We could use Hazelcast as a Cache provider directly, but we want to configure it so that we can use the Spring abstraction instead.

To use the cache we have to do two things:

  • configure the cache provider, and
  • put some annotations on the methods and classes, that should read from and modify the cache.

Configuring Hazelcast as a Cache Provider

To use the cache, we don’t need to know the cache provider. To configure the cache, however, we need to select a specific provider and configure it accordingly.

To add Hazelcast as a cache provider we first have to add Hazelcast libraries:

compile("com.hazelcast:hazelcast:4.0.1")
compile("com.hazelcast:hazelcast-spring:4.0.1")

The first dependency is the Hazelcast library, and the second one is the implementation of the Spring cache abstraction - amongst others, the implementation of CacheManager and Cache.

Now Spring Boot will find Hazelcast on the classpath and will search for a Hazelcast configuration.

Hazelcast supports two different cache topologies. We can choose which topology we want to configure.

Configuring an Embedded Cache

With the embedded topology, every instance of the Spring Boot application starts a member of the cache cluster.

Since we added Hazelcast to the classpath, Spring Boot will search for the cache configuration of Hazelcast. Spring Boot will set up the configuration for embedded topology if hazelcast.xml or hazelcast.yaml is found on the classpath. In these files, we can define cache names, data structures, and other parameters of the cache.

Another option is to configure the cache programmatically via Spring’s Java config:

import com.hazelcast.config.Config;

@Configuration
@EnableCaching
class EmbeddedCacheConfig {

  @Bean
  Config config() {
    Config config = new Config();

    MapConfig mapConfig = new MapConfig();
    mapConfig.setTimeToLiveSeconds(300);
    config.getMapConfigs().put("cars", mapConfig);

    return config;
  }
}

We add a bean of type Config to the Spring context. This is enough to configure a Hazelcast cache. The Spring cache abstraction will find this configuration and set up a Hazelcast cache with the embedded topology.

Configuring a Client-Server Cache

In Hazelcast’s Client-Server topology the application is a client of a cache cluster.

Spring’s cache abstraction will set up the client-server configuration if hazelcast-client.xml or hazelcast-client.yaml is found on the classpath. Similar to the embedded cache we can also configure the client-server topology programmatically:

@Configuration
@EnableCaching
class ClientCacheConfig {

  @Bean
  ClientConfig config() {
    ClientConfig clientConfig = new ClientConfig();
    clientConfig.addNearCacheConfig(nearCacheConfig());
    return clientConfig;
  }

  private NearCacheConfig nearCacheConfig() {
    NearCacheConfig nearCacheConfig = new NearCacheConfig();
    nearCacheConfig.setName("cars");
    nearCacheConfig.setTimeToLiveSeconds(300);
    return nearCacheConfig;
  }
}

We added the ClientConfig bean to the context. Spring will find this bean and configure the CacheManager to use Hazelcast as a client of a Hazelcast cache cluster automatically. Note that it makes sense to use near-cache in the client-server topology.

Using the Cache

Now we can use the Spring caching annotations to enable the cache on specific methods. For demo purposes, we’re looking at a Spring Boot application with an in-memory database and JPA for accessing the database.

We assume that the operations for accessing the database are slow because of heavy database use. Our goal is to avoid unnecessary operations by using a cache.

Putting Data into the Cache with @Cacheable

We create a CarService to manage car data. This service has a method for reading data:

@Service
class CarService {

  public Car saveCar(Car car) {
    return carRepository.save(car);
  }

  @Cacheable(value = "cars")
  public Car get(UUID uuid) {
    return carRepository.findById(uuid)
      .orElseThrow(() -> new IllegalStateException("car not found"));
  }
   
  // other methods omitted. 
}

The method saveCar() is supposed to be used only for inserting new cars. Normally we don’t need any cache behavior in this case. The car is just stored in the database.

The method get() is annotated with @Cachable. This annotation starts the powerful Spring cache support. The data in the cache is stored using a key-value pattern. Spring Cache uses the parameters of the method as key and the return value as a value in the cache.

When the method is called the first time, Spring will check if the value with the given key is in the cache. It will not be the case, and the method itself will be executed. It means we will have to connect to the database and read data from it. The @Cacheable annotation takes care of putting the result into the cache.

After the first call, the cached value is in the cache and stays there according to the cache configuration.

When the method is called the second time, and the cache value has not been evicted yet, Spring will search for the value by the key. Now it hits.

The value is found in the cache, and the method will not be executed.

Updating the Cache with @CachePut

The data in the cache is just a copy of the data in the primary storage. If this primary storage is changed, the data in the cache may become stale. We can solve this by using the @CachePut annotation:

@Service
class CarService {

  @CachePut(value = "cars", key = "#car.id")
  public Car update(Car car) {
    if (carRepository.existsById(car.getId())) {
      return carRepository.save(car);
    }
    throw new IllegalArgumentException("A car must have an id");
  }
  
  // other methods omitted.
}

The body of the update() method will always be executed. Spring will put the result of the method into the cache. In this case, we also defined the key that should be used to update the data in the cache.

Evicting Data from the Cache with @CacheEvict

If we delete data from our primary storage, we would have stale data in the cache. We can annotate the delete() method to update the cache:

@Service
class CarService {

  @CacheEvict(value = "cars", key = "#uuid")
  public void delete(UUID uuid) {
    carRepository.deleteById(uuid);
  }
  // Other methods omitted.
}

The @CacheEvict annotation deletes the data from the cache. We can define the key that is used to identify the cache item that should be deleted. We can delete all entries from the cache if we set the attribute allEntries to true.

Customizing Key Generation

Spring Cache uses SimpleKeyGenerator to calculate the key to be used for retrieving or updating an item in the cache from the method parameters. It’s also possible to define a custom key generation by specifying a SpEL expression in the key attribute of the @Cacheable annotation.

If that is not expressive enough for our use case, we can use a different key generator. For this, we implement the interface KeyGenerator and declare an instance of it as a Spring bean:

@Configuration
@EnableCaching
class EmbeddedCacheConfig {

  @Bean
  public KeyGenerator carKeyGenerator() {
    return new CarKeyGenerator();
  }

  // other methods omitted
}

Then, we can reference the key generator in the keyGenerator attribute of the @Cacheable annotation by bean name:

@Service
class CarService {

  @Cacheable(value = "cars", keyGenerator = "carKeyGenerator")
  public Car get(UUID uuid) {
    return carRepository.findById(uuid)
        .orElseThrow(() -> new IllegalStateException("car not found"));
  }
   
   // other methods omitted. 
}

Conclusion

Spring’s cache abstraction provides a powerful mechanism to keep cache usage abstract und independent of a cache provider.

Spring Cache supports a few well-known cache providers, which should be configured in a provider-specific way.

With Spring’s cache abstraction we can keep our business code and the cache implementation separate.

You can play around with a complete Spring Boot application using the Cache abstraction on GitHub.

Written By:

Artur Kuksin

Written By:

Artur Kuksin

With many years of experience in software development I am always looking to learn new things. I like coding and exchanging knowledge.

Recent Posts

Guide to JUnit 5 Functional Interfaces

In this article, we will get familiar with JUnit 5 functional interfaces. JUnit 5 significantly advanced from its predecessors. Features like functional interfaces can greatly simplify our work once we grasp their functionality.

Read more

Getting Started with Spring Security and JWT

Spring Security provides a comprehensive set of security features for Java applications, covering authentication, authorization, session management, and protection against common security threats such as CSRF (Cross-Site Request Forgery).

Read more

Creating and Publishing an NPM Package with Automated Versioning and Deployment

In this step-by-step guide, we’ll create, publish, and manage an NPM package using TypeScript for better code readability and scalability. We’ll write test cases with Jest and automate our NPM package versioning and publishing process using Changesets and GitHub Actions.

Read more