Caching in Spring Boot with Ehcache

Caching in Spring Boot with Ehcache

In the era of digital transformation, speed and performance have become the cornerstone of successful applications. Commonly, applications encounter scenarios where certain operations are frequently repeated, returning the same data and leading to unnecessary strain on resources. This is where caching steps in, serving as one of the best tools for performance optimization. This article explores how to utilize caching in a Spring Boot application.

What is Caching?

Caching involves storing the results of costly operations and reusing them when the same operations are carried out again. In the context of web applications, caching can significantly accelerate operations such as database queries, API calls, or intensive computations by storing the result and re-serving it instead of recomputing the operation from scratch.

Caching in Spring Boot

Spring Boot offers a very convenient abstraction for handling caching. Key annotations play a significant role in the cache mechanism. Let's explore these in the context of our movie database application.

@Cacheable Annotation

The @Cacheable annotation, which can be added to any method in your application, holds a vital place in the cache mechanism. The values returned by these methods are stored in the cache, with a key generated based on the method's arguments. If the method is invoked again with the same arguments, Spring will return the value directly from the cache instead of executing the method.

For instance, consider a method to find a movie by its title:

@Cacheable("movies")
public Movie findMovie(String title) {
    Movie movie = movieRepository.findMovieByTitle(title);
    return movie;
}

The @Cacheable annotation tells Spring that the result of the findMovie(String title) method should be cached in the "movies" cache. The first time the method is invoked with a particular movie title, the movieRepository.findMovieByTitle(String title) method is called and a Movie object is returned. This object is also stored in the "movies" cache before being returned.

The next time findMovie(String title) is called with the same movie title, Spring will check the "movies" cache before invoking the method. If it finds a Movie object in the cache associated with that title, it will return that object instead of invoking the method, saving time and resources.

@CacheEvict Annotation

This annotation is used when some data in the cache is no longer needed and should be removed. Let's consider a method to remove a movie by its title:

@CacheEvict("movies")
public void removeMovie(String title) {
    // Remove the movie from the database
    movieRepository.deleteMovieByTitle(title);
}

When the removeMovie method is invoked, it will also remove the corresponding Movie object from the "movies" cache, ensuring that the cache does not hold stale data.

@CachePut Annotation

This annotation is utilized to update the cache without interfering with the method execution. The annotation always causes the method to be invoked and its result to be stored in the cache.

Consider a method to update a movie's details:

@CachePut("movies")
public Movie updateMovie(Movie movie) {
    // Update the movie in the database and return the updated one
    Movie updatedMovie = movieRepository.updateMovie(movie);
    return updatedMovie;
}

When the updateMovie method is called, it updates the movie details in the database, and also replaces the Movie object in the "movies" cache with the updated one.

@Caching Annotation

This annotation allows you to group multiple annotations of the types mentioned above on a method.

Consider a method to save a new movie to the database:

@Caching(evict = @CacheEvict("allMoviesCache"), put = @CachePut("movieCache"))
public Movie saveMovie(Movie movie) {
    // Save the movie in the repository and return the saved movie
    Movie savedMovie = movieRepository.save(movie);
    return savedMovie;
}

When the saveMovie method is invoked, it will save the movie in the database, remove all entries in the "allMoviesCache" and add the saved movie to the "movieCache".

@CacheConfig Annotation

This annotation allows multiple cache annotations on a class to share common settings specified at the class level.

For instance:

@CacheConfig(cacheNames={"movies"})
public class MovieService {
    // Here the methods don't need to specify the cache name
}

These methods can be used in combination to effectively manage how your application interacts with your cache.

Initial Setup

Before implementing caching, let's set the stage with a basic Spring Boot application that deals with movies. Our Movie class is a simple serializable class containing a movie title.

@Data
public class Movie implements Serializable {
    private String title;

    public Movie(String title) {
        this.title = title;
    }
}

To expose our functionalities, we have a very simple MovieController, which allows us to find and delete movies by their title passed to the method by a @RequestParam.

@RestController
@RequestMapping("/movies")
@Slf4j
@AllArgsConstructor
public class MovieController {

    private final MovieService movieService;

    @GetMapping
    public ResponseEntity<Movie> findMovie(@RequestParam String title) {
        log.info("Get movie {}", title);
        Movie movie = movieService.findMovie(title);
        log.info(movie.toString());
        return ResponseEntity.ok(movie);
    }

    @DeleteMapping
    public void deleteMovie(@RequestParam String title) {
        log.info("Delete movie {}", title);
        movieService.deleteMovie(title);
        log.info("{} deleted", title);
    }
}

The actual logic of retrieving and removing movies is abstracted in a MovieService. Here, the @Cacheable annotation is added to the findMovie method, indicating that the result of this method should be stored in the cache. In contrast, the @CacheEvict annotation on the deleteMovie method signifies that once a movie is deleted, it should also be removed from the cache.

The key parameter in the @Cacheable and @CacheEvict annotations in the MovieService class specifies the key under which the cached result of a method is stored in the cache. In this case, the key is the title of the movie. So when the findMovie method is called with a particular title, Spring uses the title as the key to store the result in the cache. When the same title is passed to the method again, Spring checks if there is a value in the cache associated with that title. If there is, it will return that cached value instead of calling the method again. The same principle applies to the deleteMovie method, with the key used to identify and remove the corresponding value from the cache.

@Service
@Slf4j
@AllArgsConstructor
public class MovieService {

    private final MovieRepository movieRepository;

    @Cacheable(value = "movies", key = "#title")
    public Movie findMovie(String title) {
        return movieRepository.findMovieByTitle(title);
    }

    @CacheEvict(value = "movies", key = "#title")
    public void deleteMovie(String title) {
        movieRepository.removeMovieByTitle(title);
    }
}

The MovieService is supported by a MovieRepository that simulates a very slow service. It's designed this way to emulate situations where calling a method is resource-consuming, either because it takes a lot of computation or it involves calling a slow external service, such as a database or a RESTful API.

@Repository
public class MovieRepository {

    public Movie findMovieByTitle(String title) {
        verySlowService();
        return new Movie(title);
    }

    public void removeMovieByTitle(String title) {
        verySlowService();
    }

    private void verySlowService() {
        try {
            long time = 5000L;
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }
}

You can call the methods exposed by the MovieController via HTTP requests. For instance, you could use a tool like curl, Postman, or your web browser to send GET requests to http://localhost:8080/movies?title=MovieTitle to find a movie, or DELETE requests to the same URL to delete a movie.

For example, if you're using curl from the command line:

  • To get a movie:

      curl http://localhost:8080/movies?title=MovieTitle
    
  • To delete a movie:

      curl -X DELETE http://localhost:8080/movies?title=MovieTitle
    

Remember to replace MovieTitle with the actual title of the movie.

Cache Configuration

Spring Boot allows easy configuration of different cache providers. Depending on your needs, you can configure an in-memory based cache (like Ehcache), an external caching service (like Redis), or even implement your own cache provider.

As for differences between Ehcache and Redis:

Ehcache is a robust, proven, and full-featured cache that can be used in distributed deployments. It's a Java-based in-process cache, meaning it runs in the same process as your application. This makes it very fast and the overhead of cache access is low. However, since it shares the same process, it also shares the same memory, and it can lead to OutOfMemoryError if not properly handled. Also, it can't be shared across multiple application instances unless you are using a distributed caching feature which could add to the complexity.

On the other hand, Redis is an in-memory data structure store, used as a database, cache, and message broker. Redis provides data replication, Lua scripting, LRU eviction, transactions, and different levels of on-disk persistence. It supports various data structures such as Strings, Hashes, Lists, Sets, and more. Unlike Ehcache, Redis runs as a separate process of the application, therefore it doesn't share memory with your application. This provides a more scalable solution compared to in-process caches, especially when working with distributed systems. You can easily share the cache between multiple instances of an application, and if one of your application instances fails, the cache is not affected. However, accessing the cache involves network communication which can be slower than in-process cache access.

In this article, we will concentrate on Ehcache and discuss Redis in the next article in the series.

Ehcache Integration

For Maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.8</version>
</dependency>
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>

In your Spring Boot application, you can configure an Ehcache CacheManager in a configuration class. This CacheManager controls all aspects of caching in your application and is a standard JCache (JSR-107) object. Here is an example configuration:

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager getCacheManager() {
        CachingProvider provider = Caching.getCachingProvider();
        CacheManager cacheManager = provider.getCacheManager();

        // Cache configuration
        CacheConfigurationBuilder<String, Movie> configurationBuilder = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(
                        String.class,
                        Movie.class,
                        ResourcePoolsBuilder
                                .heap(1000)
                                .offheap(25, MemoryUnit.MB))
                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofHours(2)));

        // Cache event listener configuration
        CacheEventListenerConfigurationBuilder cacheEventListener = CacheEventListenerConfigurationBuilder
                .newEventListenerConfiguration(
                        new CacheEventLogger(),
                        EventType.CREATED,
                        EventType.EXPIRED,
                        EventType.UPDATED,
                        EventType.EVICTED,
                        EventType.REMOVED
                )
                .unordered()
                .asynchronous();

        // Create cache
        cacheManager.createCache(
            "movies",
            Eh107Configuration.fromEhcacheCacheConfiguration(
                configurationBuilder.withService(cacheEventListener)
            )
        );

        return cacheManager;
    }
}

This class sets up an Ehcache caching configuration programmatically. It creates a cache where the keys are String objects and their values are Movie objects. The cache is set up to store up to 1000 objects in heap memory and up to 25 MB in off-heap memory. The expiry policy is set up to evict entries 2 hours after creation. It also logs events for each cache operation. For instance, when an entry is created, expired, updated, evicted, or removed, an event is logged. These events are handled asynchronously and in an unordered manner.

For logging these events, a custom CacheEventLogger class is used:

@Slf4j
public class CacheEventLogger implements CacheEventListener<Object, Object> {

    @Override
    public void onEvent(CacheEvent<?, ?> cacheEvent) {
        log.info("Key: {}, EventType: {}, Old value: {}, New value: {}",
                cacheEvent.getKey(),
                cacheEvent.getType(),
                cacheEvent.getOldValue(),
                cacheEvent.getNewValue());
    }
}

This class implements the CacheEventListener interface and overrides the onEvent method. It logs the key, event type, old value, and new value for each event in the cache.

Testing out implementation

Now, let's execute our GET request to search for the movie Titanic and take a closer look at the following snippet from our application logs:

2023-07-23T22:36:32.613+02:00  INFO 3056 --- [nio-8080-exec-1] com.samulewski.cache.MovieController     : Get movie Titanic
2023-07-23T22:36:37.648+02:00  INFO 3056 --- [e [_default_]-0] com.samulewski.cache.CacheEventLogger    : Key: Titanic, EventType: CREATED, Old value: null, New value: Movie(title=Titanic)
2023-07-23T22:36:37.648+02:00  INFO 3056 --- [nio-8080-exec-1] com.samulewski.cache.MovieController     : Movie(title=Titanic)
2023-07-23T22:37:43.436+02:00  INFO 3056 --- [nio-8080-exec-5] com.samulewski.cache.MovieController     : Get movie Titanic
2023-07-23T22:37:43.438+02:00  INFO 3056 --- [nio-8080-exec-5] com.samulewski.cache.MovieController     : Movie(title=Titanic)

The logs provide a practical illustration of how our caching mechanism operates. During the first request at 22:36:32, the application retrieves the movie "Titanic" from a slow service, which incurs a certain delay. However, this process triggers a cache event, signified by the CREATED EventType at 22:36:37, showing that the movie "Titanic" has now been stored in our cache.

When a second request for the same movie is made at 22:37:43, we don't see any new cache events being triggered. Additionally, the response time is significantly reduced. The movie data was retrieved from the cache, demonstrating that the caching mechanism has functioned as intended. The cache had successfully stored the movie data during the first request, and was able to provide it much more quickly upon the second request.

Similarly, you can see that the movie is deleted from the cache when the key is found during our DELETE request:

2023-07-23T22:55:24.519+02:00  INFO 3240 --- [e [_default_]-0] com.samulewski.cache.CacheEventLogger    : Key: Titanic, EventType: REMOVED, Old value: Movie(title=Titanic), New value: null

Conclusion

In this first part of the "Caching in Spring Boot" series, we explored the powerful capabilities of Ehcache as a caching mechanism for Spring Boot applications. We demonstrated its usage with a detailed step-by-step example involving a movie search service, highlighting key features such as cache configuration, off-heap storage, expiry policies, and cache event logging. The examples provided a hands-on approach to understanding how Ehcache effectively optimizes performance by reducing expensive database operations.

The focus on Ehcache in this article sets the stage for subsequent discussions on different caching solutions that can be integrated with Spring Boot. We will continue this journey in the next article by exploring Redis, a popular in-memory data structure store, known for its performance and versatility. We will provide similar in-depth coverage, demonstrating its integration with Spring Boot and comparing its features with Ehcache.

Stay tuned for the upcoming parts of the "Caching in Spring Boot" series, where we will continue to equip you with the knowledge and skills to leverage the power of caching to boost the performance of your Spring Boot applications.

Read more

To learn more about the cache abstraction and Ehcache configuration details, please refer to the official Spring Docs and Ehcache Documentation.

Did you find this article valuable?

Support Jarosław Samulewski by becoming a sponsor. Any amount is appreciated!