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 starter to pull in Spring Data Redis and the Lettuce driver (the default client since Spring Boot 2.x).

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Configure connection details in application.properties:

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
Spring Boot auto-configures a RedisConnectionFactory, RedisTemplate, and StringRedisTemplate as soon as the starter is on the classpath. No additional bean declarations are needed for basic usage.
Lettuce 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:

XML
<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>
Jedis uses blocking I/O and requires a connection pool for thread safety. If you switch to Jedis, make sure to configure the pool settings (see the Production Tips section).

RedisTemplate

RedisTemplate<K, V> is the central class for Redis operations. Spring Boot auto-configures two beans: RedisTemplate<Object, Object> and StringRedisTemplate (keys and values are both Strings).

StringRedisTemplate: String operations
Java
@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
Java
@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
Java
@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
Java
// 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 and annotate methods to cache their return values.

Enable caching
Java
@Configuration
@EnableCaching
public class CacheConfig {
    // Spring Boot auto-configures RedisCacheManager when
    // spring-boot-starter-data-redis is on the classpath
}
@Cacheable, @CacheEvict, @CachePut
Java
@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
Java
@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() prevents null results from being stored in Redis, which avoids cache-poisoning scenarios where a missing record is cached indefinitely.

Session Management

Spring Session backed by Redis lets you share HTTP sessions across multiple application instances without sticky sessions on the load balancer.

Dependency
XML
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
Configuration
Java
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 minutes
public class SessionConfig {
    // Spring Session automatically replaces the default HttpSession
    // with a Redis-backed implementation
}
Properties
# 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
Java
@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";
    }
}
All objects stored in the session must be Serializable. If you store complex domain objects, make sure they implement java.io.Serializable or configure a JSON serializer for Spring Session.

Pub/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
Java
@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
Java
@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
Java
@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;
    }
}
Redis Pub/Sub is fire-and-forget. Messages are not persisted and will be lost if no subscriber is listening at the time of publishing. For durable messaging consider Redis Streams or a dedicated message broker like Kafka.

Serialization

By default, RedisTemplate uses JDK serialization, which produces unreadable binary data in Redis and ties you to Java. Switching to JSON serialization is strongly recommended.

Jackson2JsonRedisSerializer

Use this when all values in a template share the same type:

Java
@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:

Java
@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;
}
When using GenericJackson2JsonRedisSerializer, the JSON includes a @class field with the fully qualified class name. If you rename or move the class, existing cached entries will fail to deserialize. Plan key eviction or versioning accordingly.

Redis Repositories

Spring Data Redis supports a repository abstraction similar to Spring Data JPA. Annotate a domain class with @RedisHash and extend CrudRepository.

Java
@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; }
}
Java
public interface UserSessionRepository extends CrudRepository<UserSession, String> {

    // Derived query — requires @Indexed on the username field
    List<UserSession> findByUsername(String username);
}
Java
@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:

Java
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {
    // Scans for interfaces extending CrudRepository
    // with @RedisHash domain classes
}
The @Indexed annotation creates a secondary index in Redis (as a Set). Without it, findByUsername will not work because Redis does not natively support field-based lookups on Hashes.

Production Tips

Connection pooling

Lettuce shares a single connection by default. For high-throughput applications, enable connection pooling:

Properties
# 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
XML
<!-- 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:

Properties
# 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:

Properties
# 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:

Java
@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);
        }
    }
}
The lock pattern above is a simplified version. For production distributed locks, use Redisson's RLock or the Redlock algorithm, which handle edge cases like clock drift and partial failures across cluster nodes.
Health check

Spring Boot Actuator automatically includes a Redis health indicator when the starter is on the classpath:

Properties
management.endpoint.health.show-details=always
management.health.redis.enabled=true

Hit /actuator/health and you will see the Redis connection status, version, and mode (standalone, sentinel, or cluster) in the response.