Skip to content
Spring Boot sb production 3 min read

Redis Caching

The in-memory cache covered in Caching lives inside one JVM, so each instance has its own copy and nothing survives a restart. Redis solves both problems: it is a fast, networked key-value store that all your application instances share, giving you a single, consistent, persistent cache. Best of all, the @Cacheable / @CacheEvict / @CachePut annotations don’t change — only the backing CacheManager does.

Dependencies

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

The Redis starter brings in Lettuce, the default non-blocking client. When spring-boot-starter-cache and Redis are both present, Spring Boot auto-configures a RedisCacheManager instead of the in-memory one.

Running Redis locally

Spin up Redis with Docker for development:

docker run --name redis -p 6379:6379 -d redis:7

Tip: Verify connectivity with docker exec -it redis redis-cli ping — it should reply PONG.

Configuration

Point Spring at Redis and select it as the cache type.

spring:
  data:
    redis:
      host: localhost
      port: 6379
      # password: ${REDIS_PASSWORD}
      timeout: 2s
  cache:
    type: redis
    redis:
      time-to-live: 10m          # default TTL for all caches
      cache-null-values: false   # don't store nulls
      key-prefix: "app::"
      use-key-prefix: true

Enable caching exactly as before:

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {
}
@Cacheable("products")
public Product findById(Long id) {
    return repository.findById(id).orElseThrow();
}

The first call stores the serialized Product in Redis under a key like app::products::42; every instance now reads the same cached value.

docker exec -it redis redis-cli keys '*'

Output:

1) "app::products::42"
2) "app::products::99"

Per-cache TTL and serialization

Different caches need different lifetimes — a product catalog can live for an hour, a session token for minutes. Customize per cache with a RedisCacheManagerBuilderCustomizer, and switch values to JSON so they are human-readable in Redis (the default uses JDK serialization, which requires Serializable and produces opaque bytes).

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;

import java.time.Duration;

@Configuration
public class RedisCacheConfig {

    @Bean
    RedisCacheConfiguration defaultCacheConfig(ObjectMapper mapper) {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))
                .disableCachingNullValues()
                .serializeValuesWith(SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer(mapper)));
    }

    @Bean
    RedisCacheManagerBuilderCustomizer cacheCustomizer() {
        return builder -> builder
                .withCacheConfiguration("products",
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1)))
                .withCacheConfiguration("sessions",
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(15)));
    }
}
CacheTTLBacking config
products1 hourper-cache override
sessions15 minutesper-cache override
anything else10 minutesdefault config

A JSON-serialized value stored under app::products::42 looks like:

docker exec -it redis redis-cli get 'app::products::42'

Output:

{"@class":"com.example.Product","id":42,"name":"Mechanical Keyboard","price":89.99}

Note: GenericJackson2JsonRedisSerializer writes the @class field so it can deserialize back to the right type. If you cache different types in one cache or evolve your DTOs, keep that type info — or use a typed serializer per cache.

Connecting to a real cluster

In production point at managed Redis (ElastiCache, Memorystore) and externalize credentials:

spring:
  data:
    redis:
      host: ${REDIS_HOST}
      port: 6379
      ssl:
        enabled: true
      password: ${REDIS_PASSWORD}
      lettuce:
        pool:
          max-active: 16
          max-idle: 8

Warning: A network round-trip is far slower than an in-process map. Cache values that are expensive enough to justify the network cost, and keep them reasonably small — large objects serialized to JSON on every read can become the new bottleneck.

Best Practices

  • Reuse the standard cache annotations; only the CacheManager changes between providers.
  • Set a sensible default TTL and override per cache for differing freshness needs.
  • Prefer JSON serialization for debuggability; keep @class type info when caching polymorphic values.
  • Externalize host, password, and TLS through profiles / environment variables.
  • Monitor Redis hit/miss and latency via Actuator metrics.
Last updated June 13, 2026
Was this helpful?