Spring Data Redis: CRUD and Caching for Production
Master Spring Data Redis with CRUD operations and caching.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Java 17+
- ✓Spring Boot 3.x
- ✓Redis server running locally or accessible remotely
- ✓Basic understanding of Spring Data and caching concepts
- Use
RedisTemplatefor low-level operations and@RepositorywithCrudRepositoryfor high-level CRUD. - Enable caching with
@EnableCachingand use@Cacheable,@CachePut,@CacheEvict. - Choose appropriate serialization: JSON for readability, Kryo/Protobuf for performance.
- Handle connection failures gracefully with retry and circuit breaker patterns.
- Monitor Redis with
INFOcommand and Spring Boot Actuator endpoints.
Imagine Redis as a super-fast whiteboard in your office. Instead of going to the filing cabinet (database) every time someone asks for a customer's phone number, you write it on the whiteboard. The first person to ask writes it; everyone else just glances at the whiteboard. Spring Data Redis is the marker and eraser set that lets your Java app use that whiteboard efficiently.
You've built a REST API that's starting to feel the load. Database queries that once took 10ms now take 200ms because the data set grew. Your users are noticing slower response times. You need a cache, and Redis is the industry standard. But Spring Data Redis isn't just about slapping @Cacheable on a method. I've seen teams bring production to its knees because they didn't understand serialization, connection management, or TTL strategies.
In this guide, I'll walk you through setting up Spring Data Redis for both CRUD operations and caching. We'll cover the real-world decisions: when to use RedisTemplate vs. Repository, how to choose serialization, and what the official docs gloss over. I'll share war stories from production—like the time a fintech startup's payment service went down because of a Redis connection leak.
By the end, you'll have a robust Redis integration that scales. You'll know how to debug common issues and avoid the mistakes I've seen junior devs make (and some senior ones too). Let's get started.
Getting Started with Spring Data Redis
First, add the dependency. If you're using Spring Boot 3.2, it's straightforward:
``xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``
For reactive support, add spring-boot-starter-data-redis-reactive. Now configure your connection in application.yml:
``yaml spring: data: redis: host: localhost port: 6379 timeout: 2000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 4 max-wait: 3000ms ``
Notice I explicitly set the connection pool. The defaults are too low for any real traffic. I once saw a startup use the defaults and their Redis became a bottleneck at 50 concurrent users. Don't let that be you.
Now, create a configuration class to customize serialization. The default JdkSerializationRedisFactory is fine for prototyping, but in production you'll want JSON for readability and cross-language compatibility.
``java @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(new ``StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; } }
Use GenericJackson2JsonRedisSerializer with @JsonTypeInfo on your domain objects to preserve type info. Without it, deserialization will fail if you have polymorphic types.
CRUD Operations with RedisTemplate
RedisTemplate is your Swiss Army knife for low-level Redis operations. It supports all Redis data structures: strings, hashes, lists, sets, sorted sets, and more. For most CRUD needs, you'll use hash operations (like a map) or simple key-value pairs.
Let's build a simple payment transaction cache. We'll store transaction objects as JSON strings keyed by transaction ID.
```java @Service public class TransactionCacheService { private final RedisTemplate<String, Object> redisTemplate; private static final String KEY_PREFIX = "txn:";
public TransactionCacheService(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; }
public void saveTransaction(String txnId, Transaction txn) { redisTemplate.opsForValue().set(KEY_PREFIX + txnId, txn, 30, TimeUnit.MINUTES); }
public Transaction getTransaction(String txnId) { return (Transaction) redisTemplate.opsForValue().get(KEY_PREFIX + txnId); }
public void deleteTransaction(String txnId) { redisTemplate.delete(KEY_PREFIX + txnId); } } ```
Notice the TTL of 30 minutes. Always set a TTL. If you don't, Redis will evict keys under memory pressure, but you lose control over which keys get evicted. By setting TTL, you ensure stale data is cleaned automatically.
For batch operations, use opsForValue().multiGet() or pipelining. Pipelining sends multiple commands without waiting for responses, drastically improving throughput.
``java public List<Transaction> getTransactions(List<String> txnIds) { List<String> keys = txnIds.stream().map(id -> KEY_PREFIX + id).collect(``Collectors.toList()); List<Object> objects = redisTemplate.opsForValue().multiGet(keys); return objects.stream().map(o -> (Transaction) o).collect(Collectors.toList()); }
If you need atomic operations (e.g., increment), use opsForValue().increment(). For distributed locks, check out RedisLockRegistry from Spring Integration.
multiGet or mget in Lua scripts. I once optimized a batch endpoint by switching from a loop of get to multiGet, reducing latency from 500ms to 20ms.Spring Data Redis Repositories
If you're coming from JPA, you'll feel at home with Redis repositories. They provide a CrudRepository interface similar to Spring Data JPA. However, there are crucial differences.
First, domain objects must be annotated with @RedisHash and have an @Id field. The repository automatically manages the key space.
```java @RedisHash("transactions") public class Transaction { @Id private String id; private String userId; private BigDecimal amount; private LocalDateTime timestamp; // getters and setters }
@Repository public interface TransactionRepository extends CrudRepository<Transaction, String> { List<Transaction> findByUserId(String userId); } ```
The findByUserId method works because Spring Data Redis uses secondary indexes. Under the hood, it maintains a set for each indexed field. This is powerful but comes with overhead: every write updates the index sets.
When to use repositories? For simple CRUD with few indexed queries. For complex operations or high write throughput, stick with RedisTemplate. Repositories hide the underlying key structure, which can make debugging harder.
One gotcha: CrudRepository.save() returns the saved entity, but it's not the same instance if you use optimistic locking (via @Version). Also, the repository does not support pagination natively; you'd need to implement custom Pageable logic.
Here's a production tip: If you use repositories, be aware that the default serialization is Jdk-based. Override it in RedisCacheConfiguration or by providing a custom RedisMappingContext.
``java @Bean public RedisMappingContext redisMappingContext() { RedisMappingContext context = new ``RedisMappingContext(); context.setKeyValueAdapter(new MappingRedisConverter(context, new SimpleIndexedPropertyValueConverter())); return context; }
But honestly, I rarely use repositories in production. They're convenient for demos but RedisTemplate gives you more control.
Implementing Caching with @Cacheable
Spring's caching abstraction is clean: annotate a method with @Cacheable, and the result is stored in Redis. But the devil is in the details.
First, enable caching:
``java @Configuration @EnableCaching public class CacheConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = ``RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .build(); } }
Now annotate your service:
```java @Service public class PaymentService { @Cacheable(value = "transactions", key = "#txnId") public Transaction getTransaction(String txnId) { // slow database call return transactionRepository.findByTxnId(txnId); }
@CachePut(value = "transactions", key = "#txn.id") public Transaction updateTransaction(Transaction txn) { // update database return transactionRepository.save(txn); }
@CacheEvict(value = "transactions", key = "#txnId") public void deleteTransaction(String txnId) { transactionRepository.deleteById(txnId); } } ```
Key generation pitfall: By default, Spring uses SimpleKeyGenerator which uses method parameters. If your method takes no arguments, the key is SimpleKey.EMPTY. This means all calls return the same cached value. Always define a custom key.
Conditional caching: Use condition and unless attributes. condition decides whether to cache, unless decides whether to skip caching the result. For example, cache only if the result is not null: @Cacheable(unless = "#result == null").
Cache eviction on update: Always evict the relevant cache when data changes. Use @CacheEvict on update/delete methods. For batch updates, use @CacheEvict(allEntries = true) to clear the entire cache region.
One more thing: @Cacheable methods are intercepted by AOP, so calling them from within the same class won't trigger the cache. Use self-injection or refactor to a separate service.
sync=true helped, but the real fix was pre-warming the cache before the sale. We used @CachePut in a scheduled task.Serialization Strategies: JSON vs. Kryo vs. Protobuf
Choosing a serialization strategy is one of the most important decisions. It affects performance, memory usage, and debuggability.
JSON (Jackson): Human-readable, easy to debug, language-agnostic. Use GenericJackson2JsonRedisSerializer or Jackson2JsonRedisSerializer with a specific type. The downside: larger payload size and slower serialization compared to binary formats. For most applications, JSON is the right choice.
Kryo: Fast and compact binary serialization. Requires registering classes. Not human-readable. If you're storing millions of small objects, Kryo can save significant memory and CPU. But debugging becomes harder—you can't just redis-cli get key and see the data.
Protobuf: Even more compact, schema-driven. Requires defining .proto files. Great for microservices communication where both ends share the schema. Overkill for simple caching.
My recommendation: Start with JSON. It's flexible and debuggable. If you hit performance issues, profile and consider Kryo. I've seen teams spend weeks optimizing serialization when the real bottleneck was network latency.
Here's how to configure Kryo with Spring Data Redis:
``java @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(new ``StringRedisSerializer()); template.setValueSerializer(new KryoRedisSerializer()); return template; }
You'd need to implement KryoRedisSerializer extending RedisSerializer. But I'll be honest: I've only used Kryo twice in 15 years. JSON covers 95% of use cases.
What about security? Jackson's default typing can be exploited if you deserialize arbitrary classes. Use GenericJackson2JsonRedisSerializer with a whitelist or @JsonTypeInfo on specific classes. Never enable default typing globally.
What the Official Docs Won't Tell You
The Spring Data Redis documentation is decent, but it leaves out several production realities. Here are the gotchas I've learned the hard way.
1. Connection leaks with RedisTemplate and transactions. If you enable transaction support (setEnableTransactionSupport(true)), every operation becomes part of a Spring transaction. If you forget to commit or rollback, connections are not returned to the pool. This can exhaust connections silently. Only enable transaction support if you genuinely need Redis operations to participate in a transaction.
2. @Cacheable on methods called from within the same class won't work. This is a classic AOP limitation. The proxy intercepts external calls only. If you call getTransaction() from another method in the same service, the cache is bypassed. Solution: inject the service into itself (circular dependency) or move cached methods to a separate bean.
3. Redis Sentinel or Cluster requires special configuration. The auto-configuration works for single-node Redis. For Sentinel, you need spring.redis.sentinel.master and spring.redis.sentinel.nodes. For Cluster, use spring.redis.cluster.nodes. But the documentation doesn't emphasize that you must also configure the correct RedisConnectionFactory bean. If you mix them up, you'll get connection errors.
4. @CacheEvict with allEntries=true is expensive. It uses the FLUSHDB command (or DEL with a pattern) which can block Redis for large caches. In a multi-tenant setup, you might accidentally clear other tenants' data. Use specific keys or key patterns instead.
5. Serialization changes require cache invalidation. If you change the serialization format (e.g., from Jdk to JSON), all existing cached data becomes unreadable. You must flush the relevant cache or keys. Plan this as part of your deployment process.
6. GenericJackson2JsonRedisSerializer can cause memory leaks. It caches class metadata in a ConcurrentHashMap. Under high load with many distinct types, this map can grow large. Monitor heap usage and consider a custom serializer with a fixed type.
7. Redis doesn't support pagination natively. If you use repositories with Pageable, Spring Data Redis fetches all matching keys and applies pagination in memory. This defeats the purpose of caching and can cause OOM. Avoid pagination on Redis; use keyspace scanning with SCAN for large datasets.
setEnableTransactionSupport(true) combined with a missing @Transactional annotation. The connections were acquired but never released. We removed transaction support and the problem vanished.Best Practices for Production Redis Caching
After years of debugging Redis in production, here are the practices that save you from 2 AM calls.
1. Set TTL on everything. Every key should have a time-to-live. If you don't set TTL, Redis will evict keys under memory pressure, but you lose control. Use @Cacheable's unless to skip caching nulls.
2. Monitor cache hit ratio. Use INFO stats to see keyspace_hits and keyspace_misses. A low hit ratio means your cache is ineffective. Adjust TTL or cache more data.
3. Use connection pooling with validation. Lettuce's pool should have testOnBorrow=true to detect stale connections. Also set maxWaitMillis to avoid threads waiting indefinitely.
4. Implement circuit breakers. When Redis is down, your application should degrade gracefully, not crash. Use Resilience4j or Spring Cloud Circuit Breaker to fall back to the database.
5. Avoid caching large objects. If a single object is >1MB, consider breaking it into smaller pieces or not caching it. Large objects increase memory pressure and network latency.
6. Use Redis Cluster for high availability. But be aware that cross-slot operations (e.g., multi-key operations) require all keys to be in the same slot. Use hash tags to co-locate related keys.
7. Secure Redis. Never expose Redis to the internet. Use a firewall, and if possible, enable Redis authentication (requirepass) and TLS. Also, rename dangerous commands like FLUSHALL and CONFIG via rename-command in redis.conf.
8. Test cache behavior under load. Use tools like JMeter or Gatling to simulate traffic. Verify that cache eviction and TTL work as expected. I've seen caches that worked fine in dev but caused OOM in production because TTL was too long.
9. Have a cache invalidation strategy. When data changes, evict the corresponding cache entries. For batch updates, consider using a message queue to broadcast cache invalidation events.
10. Log cache operations. Enable debug logging for org.springframework.data.redis.cache during development. In production, log cache misses and evictions to spot issues.
The Case of the Vanishing Redis Connections
maxTotal set to 8. As traffic spiked, threads blocked waiting for a connection, causing cascading timeouts.maxTotal to 50 and maxWaitMillis to 5000ms. Also added connection validation with testOnBorrow=true.- Always configure connection pooling explicitly for production Redis.
- Monitor connection pool metrics via Actuator or JMX.
- Use circuit breakers (Resilience4j) to fail fast when Redis is slow.
- Set reasonable timeouts: connectTimeout, readTimeout, and pool wait time.
- Load test your cache layer before going live.
RedisTemplate's enableTransactionSupport carefully—it can block. Check network latency with redis-cli --latency. Optimize by using pipelining or batching.INFO command. Ensure connection pool settings are adequate. Look for firewall or DNS issues. Use spring.redis.timeout to set a reasonable connect timeout.maxmemory-policy allkeys-lru or volatile-lru. Monitor with INFO memory and set alerts on used_memory.TTL mykeyOBJECT idletime mykey| File | Command / Code | Purpose |
|---|---|---|
| RedisConfig.java | @Configuration | Getting Started with Spring Data Redis |
| TransactionCacheService.java | @Service | CRUD Operations with RedisTemplate |
| TransactionRepository.java | @RedisHash("transactions") | Spring Data Redis Repositories |
| PaymentService.java | @Service | Implementing Caching with @Cacheable |
| RedisConfigKryo.java | @Bean | Serialization Strategies |
| CacheEvictExample.java | @CacheEvict(value = "transactions", allEntries = true) | What the Official Docs Won't Tell You |
| CacheMetrics.java | management: | Best Practices for Production Redis Caching |
Key takeaways
RedisTemplate for fine-grained control and @Cacheable for declarative caching; each has its place.Interview Questions on This Topic
Explain how Spring's @Cacheable works under the hood. What happens when a method annotated with @Cacheable is called?
key attribute, and checks if the cache contains that key. If yes, it returns the cached value without executing the method. If not, it invokes the method, stores the result in the cache, and returns it. The cache manager and serializer determine how data is stored in Redis.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Hibernate & JPA. Mark it forged?
7 min read · try the examples if you haven't