← Back to the section

Spring Boot works with Redis out of the box — just add the dependency, point it at an address, and pick the tool you need: manual operations through RedisTemplate, automatic caching through @Cacheable, or session storage through Spring Session.

the annotation works through a proxy: the body runs only on a miss caller findById(42) update(product) proxy @Cacheable Redis products::42no such key products::421 hour left to live GET GET GET DEL miss: run the method body miss: run the method body method body → database SET products::42 SET products::42 method body runs: 0 1 2 miss: no key — Spring ran the body and stored the answer hit: the answer came from Redis, the body never ran @CacheEvict removed the key — the cache is empty again another miss: the method body ran a second time

A proxy created by @EnableCaching sits between the caller and the method. It asks Redis for the key products::42 and enters the body only when the key is missing; it also writes the result back with a time to live. Two consequences follow: a call made from inside the same class bypasses the proxy and is never cached, and dropping a key before it expires is the job of @CacheEvict.

Connecting and the Lettuce client

By default, Spring Data Redis uses the Lettuce client — asynchronous, thread-safe, built on Netty. Add the dependency:

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

Configure it in application.yml:

spring:
  data:
    redis:
      host: localhost
      port: 6379
      # password: secret   # if authentication is enabled
      # database: 0        # logical database number (0 by default)
      timeout: 2s

There is deliberately no connection pool here. Lettuce keeps one connection and multiplexes every command over it — enough for the vast majority of applications. A pool (lettuce.pool) is for rare cases such as blocking commands or transactions, and it does not switch itself on: without commons-pool2 on the classpath the settings are ignored.

Spring Boot automatically creates a LettuceConnectionFactory and, with it, two templates: RedisTemplate<Object, Object> and StringRedisTemplate. You only need to override the connection bean for non-standard configurations (cluster, Sentinel, TLS).

RedisTemplate and StringRedisTemplate

RedisTemplate is a general-purpose tool for direct operations with Redis. It exposes "operations" grouped by data structure type:

@Service
public class CounterService {

    private final StringRedisTemplate redis;

    public CounterService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    public void increment(String key) {
        redis.opsForValue().increment(key);
        redis.expire(key, Duration.ofHours(1));
    }

    public Long get(String key) {
        String value = redis.opsForValue().get(key);
        return value != null ? Long.parseLong(value) : 0L;
    }
}

StringRedisTemplate is a RedisTemplate<String, String> with string serialization. It fits when both the key and the value are strings.

Operations by structure type:

MethodRedis data type
opsForValue()String (scalar value)
opsForHash()Hash
opsForList()List
opsForSet()Set
opsForZSet()Sorted Set

Gotcha: default serialization

If you use RedisTemplate<String, Object> without configuring it, values are stored via JdkSerializationRedisSerializer. The data in Redis looks like an unreadable byte stream and is tied to specific Java classes — if the class changes, deserialization breaks.

The right approach is to configure JSON serialization:

@Configuration
public class RedisConfig {

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

        PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator.builder()
            .allowIfSubType("com.example.")
            .build();

        ObjectMapper mapper = new ObjectMapper();
        mapper.activateDefaultTyping(validator, ObjectMapper.DefaultTyping.NON_FINAL);

        Jackson2JsonRedisSerializer<Object> serializer =
            new Jackson2JsonRedisSerializer<>(mapper, Object.class);

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        return template;
    }
}

The list of allowed packages is mandatory: without it Jackson rebuilds an object of any class whose name is written inside the value itself — and whoever can write to Redis runs code inside the application. After this setup the values sit in Redis as readable JSON.

Spring Cache on top of Redis

Spring Cache is an abstraction over a cache: a method is marked with an annotation, and Spring decides whether to go to the database or return the stored result.

Dependency and enabling

implementation 'org.springframework.boot:spring-boot-starter-cache'
@SpringBootApplication
@EnableCaching
public class Application { ... }

@Cacheable, @CacheEvict, @CachePut

@Service
public class ProductService {

    @Cacheable(value = "products", key = "#id")
    public Product findById(Long id) {
        // called only on a cache miss
        return repository.findById(id).orElseThrow();
    }

    @CacheEvict(value = "products", key = "#product.id")
    public void update(Product product) {
        repository.save(product);
        // the cache entry for this key is evicted
    }

    @CachePut(value = "products", key = "#result.id")
    public Product create(Product product) {
        return repository.save(product);
        // the method always runs, the result is written to the cache
    }

    @CacheEvict(value = "products", allEntries = true)
    public void invalidateAll() {
        // clear the entire products cache
    }
}

The mechanics show up without Spring: the proxy is a wrapper that looks into the store by key and enters the method body only when nothing is there.

live example

import java.util.HashMap;
import java.util.Map;

public class CacheProxyDemo {

    static int bodyCalls = 0;

    public static void main(String[] args) {
        Map<String, String> redis = new HashMap<>();

        System.out.println("1) " + findById(redis, 42));
        System.out.println("2) " + findById(redis, 42));

        redis.remove("products::42");
        System.out.println("@CacheEvict removed products::42");

        System.out.println("3) " + findById(redis, 42));
        System.out.println("method body runs: " + bodyCalls);
    }

    static String findById(Map<String, String> redis, int id) {
        String key = "products::" + id;
        String cached = redis.get(key);
        if (cached != null) {
            return cached + "  <- from Redis";
        }
        bodyCalls++;
        String value = "{\"id\":" + id + ",\"title\":\"Widget\"}";
        redis.put(key, value);
        return value + "  <- from the method body";
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

RedisCacheManager and TTL

By default the cache lives forever. You can set a TTL through RedisCacheManager:

@Configuration
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration defaults = RedisCacheConfiguration
            .defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer())
            );

        Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
            "products", defaults.entryTtl(Duration.ofHours(1)),
            "sessions", defaults.entryTtl(Duration.ofMinutes(5))
        );

        return RedisCacheManager.builder(factory)
            .cacheDefaults(defaults)
            .withInitialCacheConfigurations(cacheConfigs)
            .build();
    }
}

RedisCacheConfiguration is the setup for a single cache, RedisCacheManager assembles them and registers them with Spring.

Sessions with Spring Session

Spring Session moves user HTTP sessions out of the application's memory and into Redis. This gives you two things:

  1. Horizontal scaling — several application instances share a common session store, so the user doesn't lose their session when switching between them.
  2. Surviving restarts — sessions don't disappear when the application restarts.

Dependency:

implementation 'org.springframework.session:spring-session-data-redis'

In application.yml:

spring:
  session:
    timeout: 30m
    redis:
      flush-mode: on-save    # write the session at the end of the request, not immediately
      namespace: "myapp:session"

The store type needs no property: Spring Boot 3 dropped store-type and picks the store from the classpath. Boot creates a RedisSessionRepository and wires in the SessionRepositoryFilter. To look sessions up by user id or to receive expiration events, switch to the extended variant with spring.session.redis.repository-type: indexed. Existing code that uses HttpSession works without changes.

What it looks like in Redis

After startup the keys in Redis look roughly like this:

# Cache via @Cacheable
> KEYS products::*
1) "products::42"
2) "products::17"

> GET "products::42"
"{\"id\":42,\"name\":\"Widget\",\"price\":9.99}"

> TTL "products::42"
(integer) 3542   # seconds until expiry

# Sessions via Spring Session
> KEYS myapp:session:*
1) "myapp:session:sessions:a3f9c..."
2) "myapp:session:sessions:b12ef..."

In short

  • Lettuce is the default client in Spring Boot; thread-safe, configured in application.yml.
  • StringRedisTemplate is for string operations; RedisTemplate<String, Object> is for complex values.
  • Changing serialization is mandatory: GenericJackson2JsonRedisSerializer instead of JdkSerializationRedisSerializer.
  • @Cacheable / @CacheEvict / @CachePut provide declarative caching; @EnableCaching turns the mechanism on.
  • RedisCacheManager configures TTL separately for each cache.
  • Spring Session moves HTTP sessions into Redis — scaling without losing sessions.

Further reading