Redis Caching in Spring Boot: RedisTemplate vs CacheManager


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 onConcurrentHashMap
).
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
Feature | RedisTemplate | CacheManager |
Control | Manual - you write cache logic | Automatic - Spring handles it |
Flexibility | High - custom operations | Limited - annotation-based |
Code Complexity | More code, explicit cache handling | Less code, declarative |
Best For | Complex caching scenarios | Simple 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:
RedisTemplate for Individual Items: Use for single product operations (get, update, delete) where you need precise control
CacheManager for Collections: Use for list operations where annotation-based caching is simpler
Separation of Concerns: Different caching strategies for different data types
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
Set TTL (Time To Live): Always configure cache expiration to prevent stale data
Handle Cache Invalidation: Clear or update cache when data changes
Monitor Cache Hit Rates: Track cache performance
Use Appropriate Data Types: Choose the right Redis data structure for your use case
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!
Subscribe to my newsletter
Read articles from John Manohar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
