Spring Boot with Redis — Spring Data Redis & Caching
Everything from RedisTemplate to @Cacheable to @RedisHash is Spring wiring on top of commands you already know from Chapters 01 and 04 — this chapter is where that wiring gets concrete in a real Spring Boot application.
Learning objectives
- Use RedisTemplate to read and write Redis data structures from Spring Boot, with a readable serialization setup.
- Wire Redis as the Spring Cache provider with @Cacheable, @CacheEvict, and @CachePut.
- Configure a different TTL per named cache using RedisCacheManager.
- Use @RedisHash and Spring Data Redis repositories to use Redis as a primary datastore, not just a cache.
Adding spring-boot-starter-data-redis gives Spring Boot an auto-configured RedisTemplate<String, Object> (or StringRedisTemplate, a String-specialized variant) connected to Redis via a driver like Lettuce. RedisTemplate exposes an operations object per data structure, mapping directly onto the commands from Chapter 01: opsForValue() for strings, opsForHash(), opsForList(), opsForSet(), opsForZSet() — each with methods that mirror the Redis commands themselves (opsForValue().set(key, value, Duration) is SET key value EX seconds, opsForZSet().add(key, value, score) is ZADD).
By default, RedisTemplate serializes keys and values using JDK serialization, which works but stores binary, unreadable blobs — inspecting a key from redis-cli shows garbage instead of the actual value. Real projects almost always configure RedisTemplate explicitly with a StringRedisSerializer for keys and a JSON serializer (GenericJackson2JsonRedisSerializer) for values, so data stored by the application stays human-readable in Redis directly, which matters constantly when debugging in production.
💻 Code example
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } // usage: redisTemplate.opsForValue().set("product:101", product, Duration.ofMinutes(5)); Product cached = (Product) redisTemplate.opsForValue().get("product:101"); redisTemplate.opsForZSet().add("leaderboard", "player:42", 4500.0);
Spring's cache abstraction (@EnableCaching, backed by a RedisCacheManager) sits on top of the same GET/SET calls Chapter 04 covered, but implements cache-aside for you declaratively instead of writing the check-cache/query-source/populate-cache sequence by hand for every method.
@Cacheable("products") on a method checks the named cache first using the method's arguments as the key; on a hit, the method body never runs at all and the cached value is returned directly; on a miss, the method runs, and its return value is cached automatically for next time — cache-aside (Chapter 04 §4.3), implemented by the framework. @CacheEvict(value = "products", key = "#id") on an update or delete method removes the corresponding entry so stale data isn't served after a write. @CachePut always runs the method body and then updates the cache with the result — useful when you want a write to keep the cache warm with the new value, rather than simply evicting the old one and waiting for the next read to repopulate it.
▲ Common mistake
Adding @Cacheable to a read method and stopping there. Without a matching @CacheEvict (or @CachePut) on whatever method actually changes that data, reads become fast and consistently wrong the moment the underlying row changes — the cache has no way to know the source of truth moved, and nothing tells it to forget the old value until its TTL (Chapter 04 §4.1, configured per-cache in §6.3) eventually expires it.
💻 Code example
@Cacheable("products") public Product getProduct(Long id) { return productRepository.findById(id).orElseThrow(); } @CacheEvict(value = "products", key = "#product.id") public Product updateProduct(Product product) { return productRepository.save(product); } @CachePut(value = "products", key = "#product.id") public Product updateProductAndKeepCacheWarm(Product product) { return productRepository.save(product); }
A single entryTtl applied globally rarely fits every cache in a real application — a product catalog might be safe to cache for an hour, while a live inventory count needs a TTL measured in seconds. RedisCacheManager.RedisCacheManagerBuilder lets you set a default TTL and override it per named cache via withCacheConfiguration(cacheName, config), directly implementing Chapter 04 §4.1's per-workload TTL choice through Spring configuration instead of a raw EXPIRE call.
💻 Code example
@Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)); Map<String, RedisCacheConfiguration> perCacheConfig = Map.of( "products", defaultConfig.entryTtl(Duration.ofHours(1)), "inventory-counts", defaultConfig.entryTtl(Duration.ofSeconds(30)) ); return RedisCacheManager.builder(factory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(perCacheConfig) .build(); }
Everything so far treats Redis as a cache sitting in front of some other source of truth. @RedisHash("session") on a POJO plus a repository interface extending CrudRepository flips that: Spring Data Redis stores and retrieves the object as a Redis hash directly (mapping onto the HSET/HGETALL commands from Chapter 01 §1.2), under a generated key like session:<id>, and Redis becomes the only copy of that data — there's no Postgres table behind it to fall back on if it's lost.
This is exactly the point where Chapter 02's persistence chapter stops being optional: for a @Cacheable-backed cache, losing the data just means the next read is a bit slower while it repopulates from the real source of truth (Chapter 02 §2.5); for a @RedisHash entity, losing the data means it's gone, so RDB/AOF settings (Chapter 02) need to be chosen deliberately here, not disabled for performance.
▲ Common mistake
Treating @RedisHash like a JPA @Entity and reaching for it as a general-purpose data layer. It doesn't give you joins, multi-entity transactions by default, or SQL's query flexibility — @Indexed fields support simple equality lookups, not the range queries or cross-entity joins a relational model handles natively. @RedisHash is a strong fit for simple, key-addressable objects with no relational structure — sessions, feature flags, a shopping cart — not for data with real relationships and query needs, which is what SQL Mastery and Transaction Mastery are built for.
💻 Code example
@RedisHash("session") public class UserSession { @Id private String id; private String userId; @TimeToLive private Long ttlSeconds = 1800L; } public interface SessionRepository extends CrudRepository<UserSession, String> { } // usage: sessionRepository.save(new UserSession(sessionId, userId)); Optional<UserSession> session = sessionRepository.findById(sessionId);
@Cacheable (cache role) | @RedisHash (primary-store role) | |
|---|---|---|
| Source of truth | Elsewhere (Postgres, another service) | Redis itself — there is no other copy |
| TTL | Always set — every entry is disposable | Optional, and often absent on core records |
| Safe to wipe? | Yes — next read just repopulates it | No — data is genuinely lost |
| Persistence (Chapter 02) | Often disabled entirely | Deliberately configured — this is where it matters |
The two aren't a spectrum to pick a midpoint on — they're two different jobs, and the same Redis instance can legitimately do both at once as long as it's clear, per key or per cache, which role applies. The question that decides it is the same one Chapter 02 §2.5 and Chapter 04 asked from the Redis side: if this specific piece of data vanished right now, would something else already have it, or is Redis the only place it ever lived?
Want a visual for this concept?
Generate a diagram tailored to “Spring Boot with Redis — Spring Data Redis & Caching” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →