Caching is an essential technique to improve the performance of applications by reducing database load and response time. In this post, we will explore how to integrate Redis as a caching layer in a Spring Boot application, running Redis inside a Docker container.

Why Use Redis for Caching?

Redis (Remote Dictionary Server) is an in-memory key-value store known for its high performance and scalability. It supports various data structures, persistence, and high availability, making it a great choice for caching in Spring Boot applications.

Setting Up Redis with Docker Compose

Update our docker-compose.yaml file with:

redis:
    image: redis:latest
    ports:
      - "6379:6379"
    volumes:
      - example-redis-data:/var/lib/redis/data
volumes:
  example-redis-data:

Adding Redis Cache to a Spring Boot Application

Step 1: Add Dependencies

In your pom.xml, add the necessary dependencies for Spring Boot caching and Redis:

org.springframework.boot
        spring-boot-starter-cache
    
    
        org.springframework.boot
        spring-boot-starter-data-redis

Step 2: Create a cache configuration with Redis

Annotate your class configuration with @EnableCaching:

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(cacheConfiguration)
                .build();
    }

    @Bean
    public SimpleKeyGenerator keyGenerator() {
        return new SimpleKeyGenerator();
    }

}

Explanation of the parameters:

  • entryTtl(Duration.ofMinutes(10)) → Defines that every entry of the cache expires after 10 minutes.
  • GenericJackson2JsonRedisSerializer() → Serializes the objects in a JSON format, granting the compatibility and readability of the data.
  • SimpleKeyGenerator → Use the key automatically generated by Spring to identify the objects on cache.

Step 3: Configure Redis Connection

Add the following properties to your application.properties or application.yml file:

spring.redis.host=localhost
spring.redis.port=6379

Step 4: Implement Caching in a Service

Create a service class that caches data using the @Cacheable annotation:

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

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }


    @Cacheable(value = "users", key = "#id")
    public UserResponseDto findById(UUID id) {
        var user = userRepository.findById(id).orElseThrow();
        return new UserResponseDto(user.getId(), user.getName());
    }
}

With caching enabled, the first request for a product ID will take time to process, but subsequent requests will return the cached value almost instantly.

Step 5: Test the Caching

  • Grab a id from a user you stored in the database.
  • With that id, you call the endpoint users/{id from user}.
  • In my tests using Bruno, the call to the database, without the cache is responding in 31ms Call without cache in 31ms
  • With the cache the response comes in 8ms. Call without cache in 8ms

Step 6: Remove Cache

If a user was deleted, we need to remove him from the cache to avoid outdated data.

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

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }


    @Cacheable(value = "users", key = "#id")
    public UserResponseDto findById(UUID id) {
        var user = userRepository.findById(id).orElseThrow();
        return new UserResponseDto(user.getId(), user.getName());
    }

    @Transactional
    @CacheEvict(value = "users", key = "#id")
    public void deleteById(UUID id) {
        userRepository.deleteById(id);
    }
}

Step 7: Refreshing Cache

If you want to guarantee that the cach will always be updated, even we a new user is created, you can use @CachePut.

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

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }


    @Cacheable(value = "users", key = "#id")
    public UserResponseDto findById(UUID id) {
        var user = userRepository.findById(id).orElseThrow();
        return new UserResponseDto(user.getId(), user.getName());
    }

    @Transactional
    @CacheEvict(value = "users", key = "#id")
    public void deleteById(UUID id) {
        userRepository.deleteById(id);
    }

    @Transactional
    @CachePut(value = "users", key = "#result.id")
    public UserResponseDto save(CreateUserDto user) {
        var newUser = new UserModel(user.name());

        var userCreated = userRepository.save(newUser);

        return new UserResponseDto(userCreated.getId(), userCreated.getName());
    }
}

Conclusion

Integrating Redis as a caching layer in Spring Boot significantly improves application performance by reducing database calls. Running Redis in a Docker container makes it easy to set up and maintain. By using Spring Boot’s caching abstraction, we can seamlessly cache frequently accessed data and enhance the user experience.


📍 Reference

💻 Project Repository

👋 Talk to me