How Caching Works in Spring Boot and Why You Should Care

How Caching Works in Spring Boot and Why You Should Care

Understanding Proxy Design Pattern Usage in Spring Boot

Introduction

Caching mostly works on the service layer in your application. We can cache the result returned by a method if the application calls it over and over again with the same parameters. The proper use of caching enables the web page to render fast, improve application performance and minimizes database access.

Cache Abstraction In Spring Boot

In Spring Boot caching is applied to the Java methods. That means whenever these methods are called, the cache abstraction applies the cache behavior to these methods. The data is returned without having to execute the target method if it's already available in the cache, otherwise the target method is called and the returned data is cached and returned to the caller. Cache abstraction also provides other operations such as updating or removing cached data.

But how all of this magical logic is implemented?

Caching via Proxy Pattern

Spring Boot applies proxy around Spring Beans where you declare the methods that should be cached using the @Cacheable annotation. This proxy will be in charge of adding caching behavior and will be used for dependency injection.

image.png

So what do you think will happen if you try to cache a private function? or when you call a method with @Cacheable annotation directly by another method of the same class? Will the caching work?

Let's create a Spring Boot project and see what will happen.

Create Spring Boot Project

Go to Spring Initializr, then create a new project with this setup

image.png

We only need these dependencies: Spring Web, Spring Data Redis, Spring Cache Abstraction.

Redis Server

We will use Docker to setup and run a Redis Server. Use the following command to run Redis inside a Docker container

docker run --name rediscache -p 6379:6379 redis:6.2-alpine redis-server --save 60 1 --requirepass foobar

Configuring Redis Cache

Open application.properties and add these 4 lines

spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=foobar

then add @EnableCaching annotation on Spring Boot main class

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CachedemoApplication {

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

}

Let's now create a controller which will call a service.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    @GetMapping("/hello")
    public String hello() {
        return helloService.helloWorld();
    }
}

Let's try first to cache the result returned by a private function.

Private Functions

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class HelloService {

    public String helloWorld() {
        return this.cachedHelloWorld();
    }

    @Cacheable(cacheNames = "helloWorldCache")
    private String cachedHelloWorld() {
        System.out.println("CachedHelloWorld - Start");
        return "Hello World!";
    }

}

If you try to call /hello endpoint you will notice that you can't see any caching behavior and that's because Spring's cache abstraction module uses proxies and you should use the caching annotations only on public methods which can be called by the proxy.

Now if we change the access modifier to public it should work, right? Let's see

Same Class Call

@Service
public class HelloService {

    public String helloWorld() {
        return this.cachedHelloWorld();
    }

    @Cacheable(cacheNames = "helloWorldCache")
    public String cachedHelloWorld() {
        System.out.println("CachedHelloWorld - Start");
        return "Hello World!";
    }

}

We can't see also any caching behavior, but why? The answer is because of how proxies work, you should never call a method annotated with @Cacheable, @CachePut or @CacheEvict directly by another method from the same class because in this case Spring proxy is never applied.

Now move the @Cacheable annotation to the helloWorld function and try it to call /hello.

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class HelloService {

    @Cacheable(cacheNames = "helloWorldCache")
    public String helloWorld() {
        return this.cachedHelloWorld();
    }

    public String cachedHelloWorld() {
        System.out.println("CachedHelloWorld - Start");
        return "Hello World!";
    }
}

You can see now that there's a caching behavior when you call the helloWorld function.

Conclusion

  • Never call a method annotated with @Cacheable, @CachePut or @CacheEvict directly by another method from the same class.

  • You should use the cache annotations only with public methods.

Thank you for reading!