Caching in Spring Boot with Redis
Matheus Bernardes Spilari

Matheus Bernardes Spilari @mspilari

About: - Software Engineer - Gamer - Curious

Location:
São Paulo, Brazil
Joined:
Apr 5, 2024

Caching in Spring Boot with Redis

Publish Date: Mar 17
2 3

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:

Enter fullscreen mode Exit fullscreen mode

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:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>
Enter fullscreen mode Exit fullscreen mode

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();
    }

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Comments 3 total

  • Unmesh Chougule
    Unmesh ChouguleMar 18, 2025

    @mspilari
    Thanks Matheus for this crisp blog. I have few questions on this as follows

    1. Why there is @Transactional on delete method in service
    2. Here we are feeding the keys for cache then do we still need the bean for SimpleKeyGenerator?
    • Matheus Bernardes Spilari
      Matheus Bernardes SpilariMar 18, 2025

      Hi @unmeshch. I'm glad that you like the blog.
      Thank you for your comment.
      Sorry if my answer was a bit long, but I love learning from others and sharing what I learn.
      Okay, here are the answers...

      1. Why there is @Transactional on delete method in service ?

      The @Transactional annotation on the deleteById method ensures that the deletion operation runs within a transaction. While Spring Data JPA’s deleteById method is already transactional, explicitly marking it as @Transactional can be useful in specific cases, such as:

      1. Consistency when performing multiple operations – If the method includes additional database operations before or after deletion (e.g., deleting related entities, logging changes, or updating another table), @Transactional ensures that all changes are either fully applied or rolled back if an error occurs.

      2. Cascading deletions in relationships – When dealing with complex entity relationships that rely on cascading deletes (@OneToMany(cascade = CascadeType.REMOVE)), a transaction ensures that all dependent deletions are executed atomically.

      3. Handling Lazy Loading Issues – If the deletion logic involves accessing lazy-loaded relationships, marking the method as @Transactional ensures that the session remains open during execution, preventing LazyInitializationException.

      However, if the method simply calls userRepository.deleteById(id), like i do in this post, adding @Transactional is not strictly necessary, as Spring Data JPA already handles transactions internally.


      1. Here we are feeding the keys for cache then do we still need the bean for SimpleKeyGenerator ?

      The SimpleKeyGenerator bean is not strictly necessary in this case.

      Spring provides a default key generator that works well for your current cache configurations, where you explicitly define the keys using SpEL expressions (e.g., key = "#id" or key = "#result.id"). Since the cache keys are being explicitly set, the default behavior of Spring's caching mechanism will correctly map them without needing a custom key generator.

      The SimpleKeyGenerator is useful when caching methods have multiple parameters and no explicit key is provided, as it generates a composite key based on method arguments. However, in this case, since each caching annotation already specifies a key, the custom SimpleKeyGenerator bean is redundant.

      That said, I like adding those beans/transactions in the post, even if they aren’t necessary, to provide a starting point on how to increment the keys or where to improve and use a transaction.

      So, unless you have other caching methods that rely on implicit key generation, you can safely remove the SimpleKeyGenerator bean without affecting the current cache functionality.

      • Unmesh Chougule
        Unmesh ChouguleMar 20, 2025

        @mspilari the long answer indeed helped me. Thanks for taking the time.
        This makes sense, you have given the example considering good practices and a base to start with in real world cases. Finding new things to learn is fun :-)
        Cheers!

Add comment