Intermediate · Accelerate
Production Implementation: Redis Cache-Aside Catalog
Wire the cache-aside slice end to end: namespaced keys, TTL bounds, DTO-only storage, and eviction on every write path.
Primary source: Spring Boot 3.5 Reference: Caching
Lesson 4 of 4
1d streakThe Architectural Question
Caching bolted on without ownership rules serves stale prices forever and stores entity graphs whose lazy fields break serialization at random.
What this guide proves
- Cache read-model DTOs, never managed entities
- Bound every entry with an explicit TTL
- Evict or update on the same transaction that writes
Resilient Caching: Fail-Open Error Handling
A critical production failure pattern is when an outage in the caching tier (e.g. Redis network partition or CPU throttle) brings down the entire business application. If Redis throws a RedisConnectionFailureException, Spring default behavior is to propagate the exception out of the controller, returning HTTP 500 Internal Server Error to the client.
A cache is an optimization, NOT the primary system of record. When the cache is unreachable, the system must fail open: log a warning, bypass the cache, and read directly from the database without failing user requests.
package com.example.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.CachingConfigurer;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ResilientCachingConfig implements CachingConfigurer {
private static final Logger log = LoggerFactory.getLogger(ResilientCachingConfig.class);
@Override
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
log.warn("Cache GET failed for cache='{}', key='{}'. Falling back to database source.",
cache.getName(), key, exception);
// Silently swallow exception so Spring executes the underlying method body!
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
log.error("Cache PUT failed for cache='{}', key='{}'. Data may not be cached.",
cache.getName(), key, exception);
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
log.error("CRITICAL: Cache EVICT failed for cache='{}', key='{}'. Key may contain stale data!",
cache.getName(), key, exception);
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
log.error("CRITICAL: Cache CLEAR failed for cache='{}'", cache.getName(), exception);
}
};
}
}Fail-Open on Reads vs Alerting on Eviction Failures
Cache Observability: Micrometer & Prometheus Metrics
Production caching requires continuous observability. Spring Boot Actuator automatically registers Micrometer metrics for CacheManager instances, publishing cache hit ratio, miss count, eviction count, and put latency.
| Metric Name | Tags | Alert Threshold | Actionable Remediation |
|---|---|---|---|
| cache.gets (result: hit) | cache: <name> | Hit ratio < 70% | Investigate key cardinality or insufficient TTL |
| cache.gets (result: miss) | cache: <name> | Sudden miss spike > 50% | Investigate cache eviction burst or stampede |
| cache.evictions | cache: <name> | High continuous evictions | Redis maxmemory exceeded; increase Redis memory |
| cache.puts | cache: <name> | Puts >> Gets | Ineffective caching; keys written but never reused |
Near-Cache: Two-Level L1 (Caffeine) & L2 (Redis) Architecture
In extreme low-latency environments (sub-millisecond p99), round-trip network hops to a Redis cluster (1–3ms) represent significant overhead. A two-level caching architecture provides the optimal balance:
1. L1 Local Cache (Caffeine): Resides in JVM heap memory. Access latency is < 100 nanoseconds. No network or serialization overhead.
2. L2 Distributed Cache (Redis): Shared across all Kubernetes pods. Prevents cold starts when new pods scale up.
To prevent L1 desynchronization across pods when an update occurs, use Redis Pub/Sub topic to broadcast eviction messages across all cluster instances when a write happens.
Disaster Recovery and Resilience Proving Checkpoints
Test cache disaster recovery by cutting network connectivity to Redis while driving traffic:
# 1. Simulate Redis outage by pausing the Redis container
docker pause redis-prod
# 2. Query the cached REST endpoint
curl -i http://localhost:8080/api/products/SKU-42
# EXPECTED: HTTP/1.1 200 OK (returned from database, no 500 error!)
# 3. Check logs for fail-open warning:
# WARN c.e.c.ResilientCachingConfig - Cache GET failed for cache='products', key='SKU-42'. Falling back to database source.
# 4. Resume Redis container
docker unpause redis-prodActive Recall Checkpoint
What is the danger of returning HTTP 500 when Redis is down, and how does CacheErrorHandler prevent it?
Hint: Contrast a temporary latency increase (DB fallback) with a complete service outage.
Show answer & explanation
Without a custom CacheErrorHandler, Redis connection exceptions propagate to the caller, taking down healthy services. CacheErrorHandler swallows read exceptions, allowing Spring to transparently fall back to the primary database.
Working Implementation
Core Spring Implementation
Redis cache-aside product views
Serve product views from Redis cache-aside: versioned key namespaces, bounded TTLs, and eviction after successful writes keep staleness measurable.
Component Boundaries
- 01PostgreSQL remains the source of truth; Redis stores serialized ProductView snapshots only, never managed entities.
- 02Cache names double as Redis key prefixes: catalog:v1:product maps to keys like catalog:v1:product::42 with a 10-minute TTL.
- 03Reads fill on miss through @Cacheable; the @CacheEvict interceptor drops the key after the write method completes successfully.
Required Evidence
- /actuator/metrics/spring.cache.gets and spring.cache.misses report hit ratio per cache name once micrometer-core is on the classpath.
- redis-cli --scan --pattern "catalog:v1:*" returns only reviewed prefixes, each carrying a positive TTL.
- Stopping Redis during business hours raises GET latency but every request still answers 200 from PostgreSQL and no write is lost.
Implementation sequence
Step-by-Step Integration
Follow these structured steps to inject Core Spring functionality cleanly across your boundaries.
Namespace caches and bound them with TTLs
CacheConfiguration.java
Flat key names collide across tenants and schema versions; a versioned cache name yields one clearable namespace, and entryTtl caps how long any snapshot misleads readers.
@Configuration
@EnableCaching
class CacheConfiguration {
@Bean
RedisCacheManagerBuilderCustomizer catalogCache() {
return builder -> builder.withCacheConfiguration(
"catalog:v1:product",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)));
}
}Verify Configuration
After the first GET /api/products/42, redis-cli --scan --pattern "catalog:v1:*" lists catalog:v1:product::42 and redis-cli TTL catalog:v1:product::42 prints a value between 1 and 600.
Cache DTO reads and evict after writes
ProductCatalogService.java
Managed entities drag lazy proxies into the serializer; caching the projection keeps payloads stable, and evicting on a successful rename prevents stale reads after updates.
@Cacheable(cacheNames = "catalog:v1:product", key = "#id")
@Transactional(readOnly = true)
ProductView view(UUID id) {
return products.findView(id).orElseThrow(ProductNotFound::new);
}
@CacheEvict(cacheNames = "catalog:v1:product", key = "#id")
@Transactional
void rename(UUID id, String name) {
products.require(id).rename(name);
}Verify Configuration
TRACE logging on org.springframework.cache.interceptor prints a miss for the first lookup and "Cache entry for key 42 found in cache" for the second; after rename, redis-cli EXISTS catalog:v1:product::42 prints 0.
Synthesis & Production Verification
Deliberate Failure Condition
- Break: Serialize a managed product entity straight into Redis with no TTL
- Observe: Serialization failures on lazy relations and price changes never appearing for hours
- Repair: Cache small DTOs with explicit TTLs and evict inside the write path so staleness has a hard upper bound.
Production Acceptance Criteria
- Second identical read hits Redis without touching the database
- A write evicts the affected key within the same request
- No Hibernate proxy ever reaches the serializer
Key Takeaway
Production caching must fail open via a custom CacheErrorHandler so Redis network outages never fail user requests, with metrics alerting on hit ratios.
Architectural & Technical Review
Core Interview Questions
Q01Why must production applications register a custom CacheErrorHandler with fail-open semantics?Show answer
A cache is an optimization, not the source of record. When Redis is down, handleCacheGetError swallows the exception and logs a warning so Spring transparently falls back to the database instead of returning HTTP 500.
Q02Which Micrometer metrics are essential for diagnosing cache health, and what does a high eviction rate indicate?Show answer
Monitor cache.gets (tagged by result:hit and miss), cache.puts, and cache.evictions. Continuous high evictions indicate that cache size or Redis maxmemory is undersized, causing premature key churn.
Q03How does a two-level near-cache (Caffeine L1 + Redis L2) eliminate network latency while preserving cluster consistency?Show answer
L1 Caffeine provides sub-microsecond in-memory reads without network serialization. L2 Redis prevents cold starts when pods scale. Redis Pub/Sub invalidation broadcasts key evictions to clear L1 across all pods when a write occurs.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Failing user HTTP requests with 500 errors when Redis is temporarily unreachable — Violates the principle that a cache is an optimization, not a single point of failure.
- Pitfall: Failing to alert on handleCacheEvictError exceptions in the cache error handler — Leaves stale data in the cache silently without notifying operational staff.
Module Certification Quiz
Prove Module Mastery
Comprehensive question set covering all lessons in this module.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources