Database & PerformanceCRITICAL7 min deep dive

Redis Cache When Necessary (and When NOT)

#Cache-Aside#Cache Stampede#TTL Jitter#Eviction Policies#Cache Invalidation

The 3-Line Engineering Rule

1. THE RULE

Cache only data with high read-to-write ratios and tolerable eventual consistency; never use Redis as a band-aid for missing database indexes.

2. WHY IT MATTERS

Over-caching dynamic data introduces stale reads, race conditions during cache invalidation, cache stampedes (thundering herd), and memory leaks.

3. PRODUCTION FIX

Use the Cache-Aside pattern with mandatory TTL + random jitter. Never cache volatile data updated on every user interaction without strict invalidation guarantees.

Architectural Blueprint & Sequence

Hover over nodes to inspect state transitions
CACHE-ASIDE PATTERN & JITTERSCRIBBLE BLUEPRINT
Read flow through Redis with DB fallback, TTL jitter, and write invalidation
#1

Application Service

GET /products/492

#2

Redis Cache

Cache Key: prod:492

#3

Cache HIT (0.8ms)

Return JSON payload directly

#4

Cache MISS

Read from DB replica

#5

PostgreSQL DB

SELECT * FROM products WHERE id=492

#6

Populate Cache

SET prod:492 EX (3600s + Jitter)

Execution Sequence & Transitions
Application ServiceRedis Cache[1. Check Cache]
Redis CacheCache HIT (0.8ms)[found]
Redis CacheCache MISS[not found]
Cache MISSPostgreSQL DB[2. Query DB]
PostgreSQL DBPopulate Cache[3. Populate Redis with TTL]
Populate CacheApplication Service[4. Return result]

Key Engineering Takeaways

  • Fix slow SQL queries with indexes first before slapping Redis on top.
  • Always set a TTL; immortal cache keys will eventually trigger out-of-memory (OOM) evictions.
  • Add random TTL jitter (`TTL = 3600 + rand(-300, 300)`) to avoid cache avalanche.
  • Protect against cache stampedes with mutex locking or early probabilistic expiration (XFetch).

The Problem & Production Reality

Redis is an extraordinary in-memory data store, but introducing a cache doubles system complexity: now you have two sources of truth that can get out of sync. Many teams add Redis to patch slow database queries instead of writing proper indexes, leading to ghost data and synchronization bugs.

Anti-Pattern: Indefinite Caching of Frequently Mutated Entities

Caching user profile balances or order statuses without TTLs or invalidation creates persistent consistency bugs where users see outdated bank balances.

ANTI-PATTERN EXAMPLE (JAVA)
// ❌ ANTI-PATTERN: No TTL, manual sync bugs
public UserAccount getAccount(String id) {
    UserAccount cached = redis.get("account:" + id);
    if (cached != null) return cached;
    UserAccount dbAcc = db.findAccount(id);
    redis.set("account:" + id, dbAcc); // ❌ NO TTL! If an async worker updates DB, cache is permanently stale
    return dbAcc;
}

Production Pattern: Cache-Aside with TTL Jitter & Invalidation

Always set an expiration time augmented with a random jitter to prevent all cached records from expiring simultaneously.

PRODUCTION-READY CODE (JAVA)
// ✅ PRODUCTION PATTERN: Cache-Aside with Jittered TTL
@Service
public class ProductCatalogService {

    private final StringRedisTemplate redis;
    private final ProductRepository productRepo;
    private static final int BASE_TTL_SECONDS = 3600;

    public ProductDto getProduct(String productId) {
        String cacheKey = "product:" + productId;
        String cachedJson = redis.opsForValue().get(cacheKey);

        if (cachedJson != null) {
            return JsonUtils.fromJson(cachedJson, ProductDto.class);
        }

        // Cache miss: query primary DB
        Product product = productRepo.findById(productId)
            .orElseThrow(() -> new NotFoundException("Product not found: " + productId));

        ProductDto dto = ProductMapper.toDto(product);

        // Add 0-300 seconds of random jitter to prevent cache avalanche
        int jitter = ThreadLocalRandom.current().nextInt(0, 300);
        redis.opsForValue().set(cacheKey, JsonUtils.toJson(dto), Duration.ofSeconds(BASE_TTL_SECONDS + jitter));

        return dto;
    }

    @Transactional
    public void updateProduct(String productId, UpdateProductRequest req) {
        productRepo.update(productId, req);
        // Evict cache immediately on write; do not attempt complex dual writes
        redis.delete("product:" + productId);
    }
}

Trade-off Analysis & Staff Recommendations

AspectPros / StrengthsCons / HazardsStaff Recommendation
Evict on Write vs Update on WriteEvicting on write is simple and immune to concurrent write reordering.The very next read will incur a cache miss.Always evict (delete) the cache key on update; let the subsequent read lazily repopulate.
Memory Eviction Policy`allkeys-lru` guarantees Redis won't crash when memory fills.Critical non-cached keys (e.g. rate limiter tokens) could be evicted.Use `volatile-lru` if Redis stores both cache and rate-limit counters; ensure all cache keys have TTL.

PR Code Review Checklist (Click to verify)