Caching with Redis
Cache Invalidation Strategies — TTL, Event-Based, Manual
Preventing cache staleness and configuring cache time-to-live settings in Redis.
Introduction
The primary challenge of caching is not storing data, but maintaining its accuracy. If the source database is updated, cached values become stale. **Cache Invalidation** is the process of removing or updating stale entries, using strategies such as Time-To-Live (TTL) expiration, event-based eviction, or manual programmatic invalidation.
In Spring Boot, invalidation is configured globally via cache properties, programmatically via eviction aspects, or natively inside Redis.
Why It Matters
Serving stale data can lead to business issues (e.g. users seeing incorrect inventory availability or outdated pricing). However, invalidating the cache too frequently will defeat its purpose and overload the database. A proper invalidation strategy maintains consistency without sacrificing scale.
Real-World Analogy
Think of a newspaper stand. A daily newspaper has a Time-To-Live (TTL) of 24 hours. After that, it is obsolete and thrown away. However, if a major political breaking event happens (Event-based), the old newspapers are immediately cleared out and replaced with a special edition before the 24 hours expire.
How It Works
Enterprise cache invalidation relies on three primary patterns:
- Time-To-Live (TTL): Passive eviction. Each key in Redis is assigned an expiration time. Once the duration is reached, Redis deletes the key.
- Event-Based Invalidation: Active eviction. When an entity is updated in the database, the application triggers a service listener event that automatically invalidates or refreshes the associated cache key.
- Programmatic Manual Invalidation: Explicitly using
redisTemplate.delete(key)or injecting theCacheManagerbean to clear specific caches dynamically based on custom application constraints.
Internal Architecture
Redis manages memory reclamation using two parallel eviction routines:
- Passive Eviction: When a client attempts to retrieve a key, Redis checks if its TTL has expired. If yes, it is deleted immediately, returning a cache miss.
- Active Eviction: A background routine checks random samples of keys with an expiration set and deletes them periodically.
- Eviction Policies (maxmemory-policy): If Redis hits its memory allocation cap, it reclaims space based on configuration rules (e.g.
volatile-lru: evicts least recently used keys with an expiration, orallkeys-lru: evicts any LRU key).
Visual Explanation
Practical Example
Here is how to configure per-cache custom TTLs in your Spring Boot Redis configuration class:
Common Mistakes
- Setting No Expiration TTLs: Storing keys with infinite TTL by default. Over time, memory usage balloons until Redis crashes.
- Mismatched Eviction Policies: Setting Redis maxmemory-policy to
noeviction. When memory is full, all subsequent cache write requests fail. - Transaction Rollback Leak: Evicting cache before updating the database. If the database transaction rolls back, the cache remains cleared but subsequent reads fetch old database values, resulting in inconsistency.
Quick Quiz
Q1: Which maxmemory-policy configuration instructs Redis to return write errors when memory capacity is reached instead of evicting old keys?
A) allkeys-lru
B) volatile-ttl
C) noeviction
D) allkeys-random
Answer: C — Under the noeviction policy, Redis returns errors on operations that attempt to allocate more memory (like SET), protecting existing cache data but blocking updates.
Q2: Why should cache evictions be executed after a database transaction successfully commits rather than before the update query?
A) Because evictions require a thread lock on database connections.
B) To prevent race conditions where a concurrent read retrieves the old database value and writes it back to the cache before the write query completes.
C) Because TTL calculations only start after commit actions.
D) To avoid serialization exceptions.
Answer: B — Evicting before update allows concurrent reads to fetch the old database state and re-populate the cache before the write commits, leaving the cache in a stale state indefinitely.
Scenario-Based Challenge
Production Scenario:
You configure a Spring Boot service to invalidate product cache on update using @CacheEvict. However, the database update is part of a transactional lifecycle. If the update fails and database rolls back, the cache has already been evicted, causing subsequent requests to hit the database for unchanged data. Conversely, if you evict before commit and a concurrent read occurs, it re-caches stale data. How do you integrate cache eviction safely with Spring database transactions?
To ensure the cache is only invalidated if the database write successfully commits, you should use Spring's transaction synchronization lifecycle hooks instead of standard aspects:
1. Publish a custom event in your service class:
2. Create a listener configured to capture this event ONLY after the transaction has successfully committed:
Debugging Exercise
Broken Configuration:
You attempted to configure Redis TTLs globally, but the settings are ignored, and all keys are created with infinite TTL (-1) in Redis:
Reason: Defining a custom RedisCacheManager bean overrides Spring Boot's auto-configured manager. However, the custom manager does not automatically read or apply the spring.cache.redis.time-to-live settings from the application properties file.
The Fix: Explicitly bind the TTL setting to the default cache configuration in the manager bean:
Interview Questions
1. Conceptual: What are the advantages and disadvantages of lazy eviction (TTL) compared to active invalidation (event-based eviction)?
Lazy eviction (TTL) is simple to configure, requires no complex code triggers, and guarantees keys expire eventually. However, data remains stale in the cache until the TTL duration lapses, leading to consistency lags. Active invalidation (event-based) eviction ensures near-perfect consistency since keys are invalidated immediately upon updates. However, it requires publishing events throughout your business logic, increasing complexity, and can cause DB load spikes if hot keys are invalidated during busy traffic windows.
2. Technical: How do you configure dynamic, per-cache TTL settings in Spring Boot's RedisCacheManager configuration?
Pass a pre-configured Map mapping cache names to custom RedisCacheConfiguration objects into the manager builder:
RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(customConfigMap)
.build();
This maps names like "productPrices" to configurations with shorter TTL durations (e.g. 1 minute) while allowing others to use default durations.
Production Considerations
In production clusters, configure Redis maxmemory-policy to volatile-lru or allkeys-lru to allow automated eviction when capacity is reached. Never use noeviction in production unless memory is guaranteed to exceed caching footprints.