Spring Boot Redis Multi-Cache: A Complete Guide
π Introduction
Caching is essential for high-performance applications, but enterprise-scale systems require more than simple @Cacheable
annotations. This guide demonstrates how to implement multiple cache regions, fine-grained invalidation strategies, and TTL-based cache expiration in Spring Boot with Redis.
What You'll Learn
- Configure multiple named caches in Spring Boot
- Master
@Cacheable
,@CachePut
, and@CacheEvict
annotations - Implement clean, scalable, domain-driven caching
- Connect Spring Boot with Redis for distributed caching
- Configure TTL (Time-to-Live) for automatic cache expiration
π Project Overview
We'll build a REST API featuring:
- User and UserProfile entities
- Separate cache regions:
users
andprofiles
- Smart cache updates and cross-cache invalidation
- Configurable TTL per cache region
βοΈ Technology Stack
- Spring Boot 3.x
- Redis with Lettuce driver
- Spring Cache Abstraction
- Spring Data JPA with H2 database
- Lombok for boilerplate reduction
- Docker for Redis deployment
π§± Project Structure
spring-redis-multi-cache/
βββ src/main/java/com/example/demo/
β βββ controller/
β β βββ UserController.java
β βββ domain/
β β βββ User.java
β β βββ UserProfile.java
β βββ service/
β β βββ UserService.java
β β βββ UserProfileService.java
β βββ repository/
β β βββ UserRepository.java
β β βββ UserProfileRepository.java
β βββ config/
β β βββ RedisConfig.java
β βββ SpringRedisMultiCacheApp.java
βββ src/main/resources/
β βββ application.properties
βββ docker-compose.yml
1οΈβ£ Domain Layer
User Entity
package com.example.demo.domain;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "users")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor @Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
}
UserProfile Entity
package com.example.demo.domain;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "user_profiles")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor @Builder
public class UserProfile {
@Id
private Long id;
private String address;
private String phone;
private String bio;
// This ID corresponds to User ID (one-to-one relationship)
}
2οΈβ£ Repository Layer
UserRepository
package com.example.demo.repository;
import com.example.demo.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
UserProfileRepository
package com.example.demo.repository;
import com.example.demo.domain.UserProfile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {
}
3οΈβ£ Service Layer - Multi-Cache Implementation
UserService
package com.example.demo.service;
import com.example.demo.domain.User;
import com.example.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
@Cacheable(value = "users", key = "#id")
public Optional<User> getUserById(Long id) {
log.info("Fetching user with id: {} from database", id);
simulateLatency();
return userRepository.findById(id);
}
@Cacheable(value = "users", key = "#email")
public Optional<User> getUserByEmail(String email) {
log.info("Fetching user with email: {} from database", email);
simulateLatency();
return userRepository.findByEmail(email);
}
@CachePut(value = "users", key = "#user.id")
public User saveUser(User user) {
log.info("Saving user: {}", user.getName());
return userRepository.save(user);
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
log.info("Deleting user with id: {}", id);
userRepository.deleteById(id);
}
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
log.info("Clearing all user cache entries");
}
private void simulateLatency() {
try {
Thread.sleep(2000); // Simulate database latency
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
UserProfileService
package com.example.demo.service;
import com.example.demo.domain.UserProfile;
import com.example.demo.repository.UserProfileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserProfileService {
private final UserProfileRepository profileRepository;
@Cacheable(value = "profiles", key = "#id")
public Optional<UserProfile> getProfileById(Long id) {
log.info("Fetching profile with id: {} from database", id);
simulateLatency();
return profileRepository.findById(id);
}
/**
* Updates profile and invalidates related user cache
* This ensures consistency when profile changes might affect user data
*/
@Caching(
put = {
@CachePut(value = "profiles", key = "#profile.id")
},
evict = {
@CacheEvict(value = "users", key = "#profile.id")
}
)
public UserProfile updateProfile(UserProfile profile) {
log.info("Updating profile for user id: {}", profile.getId());
return profileRepository.save(profile);
}
/**
* Deletes profile and clears both profile and user caches
*/
@Caching(evict = {
@CacheEvict(value = "profiles", key = "#id"),
@CacheEvict(value = "users", key = "#id")
})
public void deleteProfile(Long id) {
log.info("Deleting profile with id: {}", id);
profileRepository.deleteById(id);
}
@CacheEvict(value = "profiles", allEntries = true)
public void clearProfileCache() {
log.info("Clearing all profile cache entries");
}
private void simulateLatency() {
try {
Thread.sleep(2000); // Simulate database latency
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
4οΈβ£ REST Controller
package com.example.demo.controller;
import com.example.demo.domain.User;
import com.example.demo.domain.UserProfile;
import com.example.demo.service.UserService;
import com.example.demo.service.UserProfileService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final UserProfileService profileService;
// User endpoints
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/users/email/{email}")
public ResponseEntity<User> getUserByEmail(@PathVariable String email) {
return userService.getUserByEmail(email)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.saveUser(user);
return ResponseEntity.ok(savedUser);
}
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
User updatedUser = userService.saveUser(user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
// Profile endpoints
@GetMapping("/profiles/{id}")
public ResponseEntity<UserProfile> getProfile(@PathVariable Long id) {
return profileService.getProfileById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PutMapping("/profiles/{id}")
public ResponseEntity<UserProfile> updateProfile(@PathVariable Long id, @RequestBody UserProfile profile) {
profile.setId(id);
UserProfile updatedProfile = profileService.updateProfile(profile);
return ResponseEntity.ok(updatedProfile);
}
@DeleteMapping("/profiles/{id}")
public ResponseEntity<Void> deleteProfile(@PathVariable Long id) {
profileService.deleteProfile(id);
return ResponseEntity.noContent().build();
}
// Cache management endpoints
@PostMapping("/cache/users/clear")
public ResponseEntity<Void> clearUserCache() {
userService.clearUserCache();
return ResponseEntity.ok().build();
}
@PostMapping("/cache/profiles/clear")
public ResponseEntity<Void> clearProfileCache() {
profileService.clearProfileCache();
return ResponseEntity.ok().build();
}
}
5οΈβ£ Redis Configuration with TTL
package com.example.demo.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.Map;
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// JSON serializer for values
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
// String serializer for keys
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// Default cache configuration
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer))
.entryTtl(Duration.ofMinutes(30)) // Default TTL: 30 minutes
.disableCachingNullValues()
.prefixCacheNameWith("app:cache:");
// Per-cache configurations with different TTLs
Map<String, RedisCacheConfiguration> cacheConfigurations = Map.of(
"users", defaultConfig
.entryTtl(Duration.ofMinutes(5))
.prefixCacheNameWith("app:users:"),
"profiles", defaultConfig
.entryTtl(Duration.ofMinutes(10))
.prefixCacheNameWith("app:profiles:")
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.transactionAware()
.build();
}
}
6οΈβ£ Application Configuration
application.properties
# Application
spring.application.name=spring-redis-multi-cache
server.port=8080
# Database (H2 for demo)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
# JPA
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Redis
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
spring.redis.timeout=2000ms
# Logging
logging.level.com.example.demo=DEBUG
logging.level.org.springframework.cache=DEBUG
logging.level.org.springframework.data.redis=DEBUG
application.yml (Alternative)
spring:
application:
name: spring-redis-multi-cache
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password: ""
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
cache:
type: redis
redis:
host: localhost
port: 6379
database: 0
timeout: 2000ms
server:
port: 8080
logging:
level:
com.example.demo: DEBUG
org.springframework.cache: DEBUG
org.springframework.data.redis: DEBUG
7οΈβ£ Docker Setup
docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7-alpine
container_name: redis-cache
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- redis_data:/data
volumes:
redis_data:
Start Redis
# Using Docker Compose
docker-compose up -d
# Or using Docker directly
docker run --name redis-cache -p 6379:6379 -d redis:7-alpine
8οΈβ£ Testing the Cache Implementation
Cache Behavior Matrix
Operation | Endpoint | Cache Action | TTL | Notes |
---|---|---|---|---|
GET /api/v1/users/{id}
|
users cache |
@Cacheable |
5 min | Cached on first access |
GET /api/v1/users/email/{email}
|
users cache |
@Cacheable |
5 min | Cached with email as key |
POST/PUT /api/v1/users
|
users cache |
@CachePut |
5 min | Updates cache after save |
DELETE /api/v1/users/{id}
|
users cache |
@CacheEvict |
- | Removes from cache |
GET /api/v1/profiles/{id}
|
profiles cache |
@Cacheable |
10 min | Cached on first access |
PUT /api/v1/profiles/{id}
|
profiles + users
|
@CachePut + @CacheEvict
|
10 min | Updates profile, evicts user |
DELETE /api/v1/profiles/{id}
|
profiles + users
|
@CacheEvict both |
- | Removes from both caches |
Testing Script
#!/bin/bash
# Base URL
BASE_URL="http://localhost:8080/api/v1"
# Test user creation
echo "Creating user..."
curl -X POST $BASE_URL/users \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","email":"john@example.com"}'
# Test caching (first call - slow)
echo "First call (cache miss)..."
time curl -X GET $BASE_URL/users/1
# Test caching (second call - fast)
echo "Second call (cache hit)..."
time curl -X GET $BASE_URL/users/1
# Test profile creation and cross-cache invalidation
echo "Creating profile..."
curl -X PUT $BASE_URL/profiles/1 \
-H "Content-Type: application/json" \
-d '{"address":"123 Main St","phone":"555-1234","bio":"Software Developer"}'
# Test profile caching
echo "Getting profile (cache miss)..."
time curl -X GET $BASE_URL/profiles/1
echo "Getting profile again (cache hit)..."
time curl -X GET $BASE_URL/profiles/1
9οΈβ£ Redis Cache Inspection
Using Redis CLI
# Connect to Redis
docker exec -it redis-cache redis-cli
# List all keys
keys *
# Check TTL for specific key
ttl "app:users:1"
# Get cached value
get "app:users:1"
# Monitor cache operations in real-time
monitor
Expected Redis Keys
app:users:1
app:users:john@example.com
app:profiles:1
π§ Advanced Configuration Options
Custom Cache Key Generator
@Component
public class CustomCacheKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName()).append(":");
key.append(method.getName()).append(":");
for (Object param : params) {
key.append(param.toString()).append(":");
}
return key.toString();
}
}
Cache Event Listener
@Component
@EventListener
public class CacheEventListener {
private static final Logger logger = LoggerFactory.getLogger(CacheEventListener.class);
@EventListener
public void handleCacheHit(CacheEvent event) {
logger.info("Cache hit: {} - {}", event.getCacheName(), event.getKey());
}
@EventListener
public void handleCacheMiss(CacheEvent event) {
logger.info("Cache miss: {} - {}", event.getCacheName(), event.getKey());
}
}
π― Key Takeaways
Cache Strategy Best Practices
- Separate Cache Regions: Use different cache names for different domains
- Appropriate TTL: Set TTL based on data volatility and business requirements
-
Cross-Cache Invalidation: Use
@Caching
for complex invalidation scenarios - Key Design: Use meaningful, unique keys that won't collide
- Null Value Handling: Disable caching of null values to avoid cache pollution
Performance Considerations
- Users Cache: 5-minute TTL for frequently changing data
- Profiles Cache: 10-minute TTL for less volatile data
- JSON Serialization: More readable and cross-platform compatible
- Connection Pooling: Lettuce provides efficient connection management
Production Readiness
- Transaction Awareness: Cache operations participate in Spring transactions
- Error Handling: Graceful fallback when cache is unavailable
- Monitoring: Enable cache metrics and logging
- Security: Configure Redis AUTH in production environments
π Next Steps
Production Enhancements
- Redis Cluster: Implement Redis Sentinel or Cluster for high availability
- Cache Warming: Pre-populate critical cache entries on startup
- Metrics Integration: Add Micrometer metrics for cache hit/miss ratios
- Security: Configure Redis AUTH and SSL/TLS
- Backup Strategy: Implement Redis persistence and backup procedures
Advanced Features
- Conditional Caching: Use SpEL expressions for dynamic cache conditions
- Cache Synchronization: Implement distributed cache synchronization
- Multi-Level Caching: Combine local and distributed caching
- Cache Aside Pattern: Manual cache management for complex scenarios
This comprehensive guide provides a solid foundation for implementing enterprise-grade caching in Spring Boot applications with Redis.
Top comments (0)