Spring Boot with Redis: Caching & Data Store
Redis is the most popular in-memory data store used with Spring Boot for caching, session management, and real-time messaging. Spring Data Redis provides RedisTemplate for low-level operations, Spring Cache abstraction for declarative caching with @Cacheable, and Spring Session for distributed HTTP sessions: all auto-configured by Spring Boot.
Contents
Dependency & Configuration
Add the spring-boot-starter-data-redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Configure connection details in application.properties
# Redis connection
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=mysecret
spring.data.redis.database=0
# Connection timeout
spring.data.redis.timeout=2000ms
RedisConnectionFactoryRedisTemplateStringRedisTemplateLettuce vs Jedis
Spring Boot defaults to Lettuce, which is non-blocking and thread-safe thanks to Netty. If you prefer Jedis, exclude Lettuce and add the Jedis dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
RedisTemplate
RedisTemplate<K, V>RedisTemplate<Object, Object>StringRedisTemplate
StringRedisTemplate: String operations
@Service
public class CacheService {
private final StringRedisTemplate redis;
public CacheService(StringRedisTemplate redis) {
this.redis = redis;
}
// opsForValue — simple key-value
public void setString(String key, String value) {
redis.opsForValue().set(key, value, Duration.ofMinutes(30));
}
public String getString(String key) {
return redis.opsForValue().get(key);
}
// increment a counter atomically
public Long incrementCounter(String key) {
return redis.opsForValue().increment(key);
}
}
opsForHash: Hash operations
@Service
public class UserProfileService {
private final StringRedisTemplate redis;
public UserProfileService(StringRedisTemplate redis) {
this.redis = redis;
}
public void saveProfile(String userId, Map<String, String> profile) {
String key = "user:profile:" + userId;
redis.opsForHash().putAll(key, profile);
redis.expire(key, Duration.ofHours(1));
}
public Map<Object, Object> getProfile(String userId) {
return redis.opsForHash().entries("user:profile:" + userId);
}
public void updateField(String userId, String field, String value) {
redis.opsForHash().put("user:profile:" + userId, field, value);
}
}
opsForList and opsForSet
@Service
public class CollectionOpsService {
private final StringRedisTemplate redis;
public CollectionOpsService(StringRedisTemplate redis) {
this.redis = redis;
}
// opsForList — ordered list (queue / stack)
public void pushToQueue(String queueName, String message) {
redis.opsForList().rightPush(queueName, message);
}
public String popFromQueue(String queueName) {
return redis.opsForList().leftPop(queueName);
}
public List<String> getRecentItems(String key, int count) {
return redis.opsForList().range(key, 0, count - 1);
}
// opsForSet — unique unordered collection
public void addTag(String articleId, String tag) {
redis.opsForSet().add("article:tags:" + articleId, tag);
}
public Set<String> getTags(String articleId) {
return redis.opsForSet().members("article:tags:" + articleId);
}
public Boolean hasTag(String articleId, String tag) {
return redis.opsForSet().isMember("article:tags:" + articleId, tag);
}
}
Delete and check existence
// delete a key
redis.delete("user:profile:42");
// delete multiple keys
redis.delete(List.of("key1", "key2", "key3"));
// check if a key exists
Boolean exists = redis.hasKey("user:profile:42");
// set TTL on an existing key
redis.expire("session:abc", Duration.ofMinutes(15));
Spring Cache with Redis
Spring Boot's cache abstraction works seamlessly with Redis. Enable it with @EnableCaching
Enable caching
@Configuration
@EnableCaching
public class CacheConfig {
// Spring Boot auto-configures RedisCacheManager when
// spring-boot-starter-data-redis is on the classpath
}
@Cacheable, @CacheEvict, @CachePut
@Service
public class ProductService {
private final ProductRepository repo;
public ProductService(ProductRepository repo) {
this.repo = repo;
}
// Cache the result — subsequent calls with the same id skip the DB
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
return repo.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
// Update the cache entry after a write
@CachePut(value = "products", key = "#product.id")
public Product update(Product product) {
return repo.save(product);
}
// Remove the cache entry when the product is deleted
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
repo.deleteById(id);
}
// Evict all entries in the "products" cache
@CacheEvict(value = "products", allEntries = true)
public void clearCache() {
// no-op — the annotation does the work
}
}
RedisCacheManager with custom TTL
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// Per-cache TTL overrides
Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
"products", defaultConfig.entryTtl(Duration.ofMinutes(30)),
"users", defaultConfig.entryTtl(Duration.ofHours(1))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
}
disableCachingNullValues()Session Management
Spring Session backed by Redis lets you share HTTP sessions across multiple application instances without sticky sessions on the load balancer.
Dependency
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
Configuration
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 minutes
public class SessionConfig {
// Spring Session automatically replaces the default HttpSession
// with a Redis-backed implementation
}
# Session store type
spring.session.store-type=redis
# Namespace prefix for session keys
spring.session.redis.namespace=myapp:session
# Flush mode — ON_SAVE (default) or IMMEDIATE
spring.session.redis.flush-mode=on-save
Using the session in a controller
@RestController
@RequestMapping("/api")
public class SessionController {
@GetMapping("/login")
public String login(HttpSession session) {
session.setAttribute("user", "alice");
session.setAttribute("loginTime", Instant.now().toString());
return "Session created: " + session.getId();
}
@GetMapping("/me")
public Map<String, Object> currentUser(HttpSession session) {
return Map.of(
"user", session.getAttribute("user"),
"loginTime", session.getAttribute("loginTime"),
"sessionId", session.getId()
);
}
@PostMapping("/logout")
public String logout(HttpSession session) {
session.invalidate();
return "Session invalidated";
}
}
Serializablejava.io.SerializablePub/Sub Messaging
Redis Pub/Sub provides lightweight messaging between services. One service publishes a message to a channel and all subscribers receive it in real time.
Publisher
@Service
public class OrderEventPublisher {
private final StringRedisTemplate redis;
public OrderEventPublisher(StringRedisTemplate redis) {
this.redis = redis;
}
public void publishOrderCreated(String orderId) {
String message = "{\"event\":\"ORDER_CREATED\",\"orderId\":\"" + orderId + "\"}";
redis.convertAndSend("orders.events", message);
}
}
Subscriber
@Component
public class OrderEventListener implements MessageListener {
private static final Logger log = LoggerFactory.getLogger(OrderEventListener.class);
@Override
public void onMessage(Message message, byte[] pattern) {
String body = new String(message.getBody());
String channel = new String(message.getChannel());
log.info("Received on [{}]: {}", channel, body);
// process the event...
}
}
Listener container configuration
@Configuration
public class RedisPubSubConfig {
@Bean
public RedisMessageListenerContainer listenerContainer(
RedisConnectionFactory connectionFactory,
OrderEventListener listener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// Subscribe to one or more channels
container.addMessageListener(listener, new ChannelTopic("orders.events"));
// Pattern-based subscription (e.g., orders.*)
container.addMessageListener(listener, new PatternTopic("orders.*"));
return container;
}
}
Serialization
By default, RedisTemplate
Jackson2JsonRedisSerializer
Use this when all values in a template share the same type:
@Bean
public RedisTemplate<String, Product> productRedisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Product> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Product.class));
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Product.class));
return template;
}
GenericJackson2JsonRedisSerializer
Use this when a single template stores multiple types. It embeds the fully qualified class name in the JSON so deserialization knows the target type:
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
GenericJackson2JsonRedisSerializer serializer =
new GenericJackson2JsonRedisSerializer(mapper);
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
GenericJackson2JsonRedisSerializer@classRedis Repositories
Spring Data Redis supports a repository abstraction similar to Spring Data JPA. Annotate a domain class with @RedisHashCrudRepository
@RedisHash(value = "session", timeToLive = 3600) // TTL in seconds
public class UserSession {
@Id
private String id;
@Indexed // enables findBy queries on this field
private String username;
private String ipAddress;
private Instant createdAt;
// constructors, getters, setters
public UserSession() {}
public UserSession(String username, String ipAddress) {
this.username = username;
this.ipAddress = ipAddress;
this.createdAt = Instant.now();
}
public String getId() { return id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
public Instant getCreatedAt() { return createdAt; }
}
public interface UserSessionRepository extends CrudRepository<UserSession, String> {
// Derived query — requires @Indexed on the username field
List<UserSession> findByUsername(String username);
}
@Service
public class SessionTrackingService {
private final UserSessionRepository repo;
public SessionTrackingService(UserSessionRepository repo) {
this.repo = repo;
}
public UserSession createSession(String username, String ip) {
UserSession session = new UserSession(username, ip);
return repo.save(session); // stored as a Redis Hash
}
public List<UserSession> getActiveSessions(String username) {
return repo.findByUsername(username);
}
public void endSession(String sessionId) {
repo.deleteById(sessionId);
}
}
Enable Redis repositories in your configuration:
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {
// Scans for interfaces extending CrudRepository
// with @RedisHash domain classes
}
@IndexedfindByUsernameProduction Tips
Connection pooling
Lettuce shares a single connection by default. For high-throughput applications, enable connection pooling:
# Lettuce pool (requires commons-pool2 on classpath)
spring.data.redis.lettuce.pool.enabled=true
spring.data.redis.lettuce.pool.max-active=16
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=2
spring.data.redis.lettuce.pool.max-wait=2000ms
<!-- Required for Lettuce connection pooling -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
Sentinel configuration
Redis Sentinel provides automatic failover for high availability:
# Sentinel mode
spring.data.redis.sentinel.master=mymaster
spring.data.redis.sentinel.nodes=sentinel1:26379,sentinel2:26379,sentinel3:26379
spring.data.redis.sentinel.password=sentinelpass
spring.data.redis.password=redispass
Cluster configuration
Redis Cluster distributes data across multiple nodes for horizontal scaling:
# Cluster mode
spring.data.redis.cluster.nodes=node1:6379,node2:6379,node3:6379,node4:6379,node5:6379,node6:6379
spring.data.redis.cluster.max-redirects=3
spring.data.redis.password=clusterpass
Key expiration strategies
Always set a TTL on cache keys to prevent unbounded memory growth. Use different strategies based on access patterns:
@Component
public class KeyExpirationService {
private final StringRedisTemplate redis;
public KeyExpirationService(StringRedisTemplate redis) {
this.redis = redis;
}
// Fixed TTL — key expires after a set duration
public void setWithFixedTtl(String key, String value) {
redis.opsForValue().set(key, value, Duration.ofMinutes(30));
}
// Sliding TTL — reset expiration on every read
public String getWithSlidingTtl(String key, Duration ttl) {
String value = redis.opsForValue().get(key);
if (value != null) {
redis.expire(key, ttl); // reset the clock
}
return value;
}
// Conditional set — only set if key does not exist (distributed lock pattern)
public boolean tryLock(String lockKey, String owner, Duration timeout) {
Boolean acquired = redis.opsForValue()
.setIfAbsent(lockKey, owner, timeout);
return Boolean.TRUE.equals(acquired);
}
public void releaseLock(String lockKey, String owner) {
String current = redis.opsForValue().get(lockKey);
if (owner.equals(current)) {
redis.delete(lockKey);
}
}
}
RLockHealth check
Spring Boot Actuator automatically includes a Redis health indicator when the starter is on the classpath:
management.endpoint.health.show-details=always
management.health.redis.enabled=true
Hit /actuator/health