Redis Caching in Spring Boot: RedisTemplate vs CacheManager

John ManoharJohn Manohar
7 min read

When building modern web applications, caching is essential for performance. Redis, combined with Spring Boot, offers powerful caching solutions. In this guide, we'll explore how to use both RedisTemplate and CacheManager effectively in your Spring Boot applications.

What is Redis?

Redis (REmote DIctionary Server) is an in-memory data store which is significantly faster than disk-based storage, making it ideal for high-speed applications. It primarily uses a key-value data model, where each piece of data is associated with a unique key.

Key: "user:123"     →  Value: {"name": "John", "email": "john@example.com"}
Key: "product:456"  →  Value: {"title": "Laptop", "price": 999.99}

Since it is extremely fast for read and write operations. It's perfect for caching frequently accessed data from your database.

Dependencies Required

To get started, add the dependency to your pom.xml:

<dependencies>
    <!-- Spring Boot Starter Web, Spring Boot Starter Data -->
     <!-- After your other dependencies, add... -->
       <!-- Redis Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

Configure Redis in your application.properties:

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

Redis Configuration

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        return template;
    }
}

RedisTemplate enables us to interact with the redis server. In order to establish a connection, we need to pass RedisConnectionFactory instance to the RedisTemplate.

Caching with RedisTemplate

RedisTemplate gives you direct control over Redis operations. You manually decide when to cache, what to cache, and how to retrieve cached data. By default, RedisTemplate uses the JdkSerializationRedisSerializer to serialize and deserialize objects.

Example: Product Service with RedisTemplate

@Service
public class ProductService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private ProductMapper productMapper;

    // GET /products/{id}
    public ProductResponseDTO getProductById(int id) {
        // 1. Check cache first
        ProductResponseDTO cachedProduct = (ProductResponseDTO) 
            redisTemplate.opsForHash().get("PRODUCTS", "PRODUCT_" + id);

        if (cachedProduct != null) {
            System.out.println("Cache HIT for product: " + id);
            return cachedProduct;
        }

        // 2. Cache miss - fetch from database
        System.out.println("Cache MISS for product: " + id);
        Optional<Product> optionalProduct = productRepository.findByIdAndIsDeletedFalse(id);

        if (optionalProduct.isEmpty()) {
            throw new ProductNotFoundException(id);
        }

        Product product = optionalProduct.get();
        ProductResponseDTO responseDTO = productMapper.productToProductResponseDTO(product);

        // 3. Store in cache
        redisTemplate.opsForHash().put("PRODUCTS", "PRODUCT_" + id, responseDTO);

        return responseDTO;
    }

    // PUT /products/{id}
    public ProductResponseDTO updateProduct(int id, ProductRequestDTO requestDTO) {
        Optional<Product> optionalProduct = productRepository.findByIdAndIsDeletedFalse(id);

        if (optionalProduct.isEmpty()) {
            throw new ProductNotFoundException(id);
        }

        Product product = optionalProduct.get();
        productMapper.updateProductFromDTO(requestDTO, product);

        Product updatedProduct = productRepository.save(product);
        ProductResponseDTO responseDTO = productMapper.productToProductResponseDTO(updatedProduct);

        // Update cache with new data
        redisTemplate.opsForHash().put("PRODUCTS", "PRODUCT_" + id, responseDTO);
        System.out.println("Cache UPDATED for product: " + id);

        return responseDTO;
    }

    // DELETE /products/{id}
    public void deleteProduct(int id) {
        Optional<Product> optionalProduct = productRepository.findByIdAndIsDeletedFalse(id);

        if (optionalProduct.isEmpty()) {
            throw new ProductNotFoundException(id);
        }

        Product product = optionalProduct.get();
        product.setIsDeleted(true);
        productRepository.save(product);

        // Remove from cache
        redisTemplate.opsForHash().delete("PRODUCTS", "PRODUCT_" + id);
        System.out.println("Cache DELETED for product: " + id);
    }
}

As we can see in the GET /products/{id} route, first we check to see if we have the requested resource in the cache. If not, we make a call to the database and fetch the resource. Before returning the requested resource, we cache it and then return so that on subsequent requests the cached resource is served, cutting down response time.

Similarly, on UPDATE, we change the cached resource to store the updated resource and on DELETE, we we delete from the cache. Let’s look at the response times.

Response time without Redis: 334 ms

With Redis: 13 ms

We cut down the response time by 95%!!!

Caching with CacheManager

CacheManager provides annotation-based caching. Spring automatically handles cache operations for you.

Dependecy required

<dependencies>
    <!-- Spring Boot Starter Web, Spring Boot Starter Data and other 
        dependencies-->
    <!-- Redis Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- Cache Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
</dependencies>

Also, also we need to tell Spring Boot to enable annotation-driven cache management. To do that, we need to add @EnableCaching annotation to our main Spring Boot application class.

@SpringBootApplication
@EnableCaching
public class ProductserviceApplication {

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

Mechanism:

  • When @EnableCaching is present on a @Configuration class (or the main application class annotated with @SpringBootApplication), it triggers a post-processor during application startup.

  • This post-processor scans all Spring beans for the presence of caching annotations on their public methods.

  • If a method is found with a caching annotation (e.g., @Cacheable), a proxy is automatically created around that method. This proxy intercepts method calls and handles the caching logic, such as checking the cache before executing the method or storing the result in the cache after execution.

@EnableCaching annotation in Spring Boot requires a CacheManager bean to be present in the application context. This CacheManager is responsible for providing the underlying caching mechanism used by Spring's caching abstraction.

If we don’t provide one, Spring Boot might automatically configure a default CacheManager (like one based on ConcurrentHashMap).

Let’s add a CacheManager Bean so that Spring Boot can control the cache behavior using Redis.

Make these changes to your @Configuration class:

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
         // create a redis cache configuration
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .prefixCacheNameWith("myapp.")
            .entryTtl(Duration.ofMinutes(10)) // Cache is automatically cleared after 10 mins
            .disableCachingNullValues(); 

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
    }
}

This CacheManager now tells Spring Boot that we are using Redis.

Example: Get All Products with CacheManager using @Cacheable

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private ProductMapper productMapper;

    // GET /products
    @Cacheable(value = "product-list", key = "'all-active-products'")
    public List<ProductResponseDTO> getAllProducts() {
        System.out.println("Fetching all products from database...");

        List<Product> products = productRepository.findByIsDeletedFalse();
        return products.stream()
            .map(productMapper::productToProductResponseDTO)
            .collect(Collectors.toList());
    }

    // Clear cache when products are modified
    @CacheEvict(value = "product-list", allEntries = true)
    public void clearProductListCache() {
        System.out.println("Product list cache cleared!");
    }
}

When a request is made to GET /products/, spring first checks the cache before invoking the method and caching the result. Let’s see the response times.

GET /products/ without redis: 241 ms

GET /products/ with redis: 11 ms

That’s 95% reduction in response time!

When UPDATE, DELETE, CREATE requests are made, we need to evict this result. For that, we use @CacheEvict annotation.

Update your update and delete methods to clear the list cache:

// In your update method, add this annotation
@CacheEvict(value = "product-list", allEntries = true)
public ProductResponseDTO updateProduct(int id, ProductRequestDTO requestDTO) {
    // ... existing update logic
    clearProductListCache(); // Clear the list cache
    return responseDTO;
}

// In your delete method, add this annotation
@CacheEvict(value = "product-list", allEntries = true)
private void deleteProduct(int id) {
    // ... existing delete logic
    clearProductListCache(); // Clear the list cache
}

Using simple annotations of @Cacheable, @CacheEvict, we can cache entire method level endpoints.

RedisTemplate vs CacheManager: Key Differences

FeatureRedisTemplateCacheManager
ControlManual - you write cache logicAutomatic - Spring handles it
FlexibilityHigh - custom operationsLimited - annotation-based
Code ComplexityMore code, explicit cache handlingLess code, declarative
Best ForComplex caching scenariosSimple method-level caching

When to Use Each

Use RedisTemplate when:

  • You need fine-grained control over caching

  • Working with complex data structures (lists, sets, hashes)

  • Implementing custom cache invalidation logic

  • Building cache-aside patterns

  • Need to perform multiple Redis operations together

Use CacheManager when:

  • Simple method-level caching

  • Want declarative caching with annotations

  • Standard cache operations (get, put, evict)

  • Less boilerplate code

  • Team prefers annotation-based approach

Why Using Both is a Great Approach

Combining RedisTemplate and CacheManager gives you the best of both worlds:

  1. RedisTemplate for Individual Items: Use for single product operations (get, update, delete) where you need precise control

  2. CacheManager for Collections: Use for list operations where annotation-based caching is simpler

  3. Separation of Concerns: Different caching strategies for different data types

  4. Flexibility: Choose the right tool for each specific use case

Complete Controller Example

@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/{id}")
    public ResponseEntity<ProductResponseDTO> getProduct(@PathVariable int id) {
        ProductResponseDTO product = productService.getProductById(id);
        return ResponseEntity.ok(product);
    }

    @GetMapping
    public ResponseEntity<List<ProductResponseDTO>> getAllProducts() {
        List<ProductResponseDTO> products = productService.getAllProducts();
        return ResponseEntity.ok(products);
    }

    @PutMapping("/{id}")
    public ResponseEntity<ProductResponseDTO> updateProduct(
            @PathVariable int id, 
            @RequestBody ProductRequestDTO requestDTO) {
        ProductResponseDTO product = productService.updateProduct(id, requestDTO);
        return ResponseEntity.ok(product);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable int id) {
        productService.deleteProduct(id);
        return ResponseEntity.noContent().build();
    }
}

Best Practices

  1. Set TTL (Time To Live): Always configure cache expiration to prevent stale data

  2. Handle Cache Invalidation: Clear or update cache when data changes

  3. Monitor Cache Hit Rates: Track cache performance

  4. Use Appropriate Data Types: Choose the right Redis data structure for your use case

  5. Don't Cache Everything: Only cache frequently accessed data

Conclusion

Redis caching with Spring Boot offers powerful performance improvements. RedisTemplate provides fine-grained control for complex scenarios, while CacheManager offers simplicity through annotations. Using both approaches strategically gives you maximum flexibility and optimal performance.

The key is understanding when to use each tool:

  • RedisTemplate: When you need control and flexibility

  • CacheManager: When you want simplicity and standard caching patterns

Start with CacheManager for simple cases, and add RedisTemplate where you need more control. Your application's performance will thank you!

0
Subscribe to my newsletter

Read articles from John Manohar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

John Manohar
John Manohar