ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

Distributed Caching Patterns

Design caching strategies that reduce latency, lower database load, and scale horizontally. Covers cache-aside, read-through, write-behind, cache invalidation, distributed cache topologies, and the patterns that make caching reliable in production.

Caching is the most effective performance optimization available. A well-designed cache reduces database load by 90%, cuts response times from 200ms to 2ms, and enables systems to handle 10x more traffic. A poorly designed cache causes stale data, cache stampedes, and harder-to-debug inconsistencies.


Caching Patterns

Cache-Aside (Lazy Loading):
  1. Check cache for data
  2. Cache miss → query database
  3. Store result in cache
  4. Return to caller
  
  Best for: Read-heavy workloads, data that tolerates staleness
  Risk: Cache miss thundering herd

Read-Through:
  1. Application reads from cache
  2. Cache miss → cache queries database itself
  3. Cache stores and returns result
  
  Best for: Simplifying application code
  Risk: Cache library must support this pattern

Write-Through:
  1. Application writes to cache
  2. Cache writes to database synchronously
  3. Both updated atomically
  
  Best for: Data that must be fresh immediately
  Risk: Write latency increased (cache + DB)

Write-Behind (Write-Back):
  1. Application writes to cache
  2. Cache writes to database asynchronously
  3. Write returns immediately after cache update
  
  Best for: Write-heavy workloads, performance-critical
  Risk: Data loss if cache fails before async write completes

Redis as Distributed Cache

import redis
import json
from functools import wraps

class DistributedCache:
    def __init__(self, redis_url, default_ttl=300):
        self.redis = redis.Redis.from_url(redis_url)
        self.default_ttl = default_ttl
    
    def get(self, key):
        data = self.redis.get(key)
        return json.loads(data) if data else None
    
    def set(self, key, value, ttl=None):
        self.redis.setex(
            key,
            ttl or self.default_ttl,
            json.dumps(value, default=str)
        )
    
    def invalidate(self, key):
        self.redis.delete(key)
    
    def invalidate_pattern(self, pattern):
        """Invalidate all keys matching pattern."""
        keys = self.redis.keys(pattern)
        if keys:
            self.redis.delete(*keys)

# Cache-aside decorator
def cached(cache, key_fn, ttl=300):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            key = key_fn(*args, **kwargs)
            
            # Check cache
            result = cache.get(key)
            if result is not None:
                return result
            
            # Cache miss: call function
            result = await func(*args, **kwargs)
            
            # Store in cache
            cache.set(key, result, ttl)
            return result
        return wrapper
    return decorator

# Usage
@cached(cache, key_fn=lambda user_id: f"user:{user_id}", ttl=600)
async def get_user(user_id: str):
    return await db.query("SELECT * FROM users WHERE id = $1", user_id)

Cache Invalidation

"There are only two hard things in computer science:
 cache invalidation and naming things." — Phil Karlton

Strategies:
  TTL (Time-to-Live):
    Data expires after fixed time
    Simple, eventual consistency
    
  Event-Based:
    When data changes → invalidate cache
    Immediate consistency, complex implementation
    
  Version-Based:
    Cache key includes version number
    user:123:v5 → update increments to v6
    Old version naturally expires via TTL

Anti-Patterns

Anti-PatternConsequenceFix
Cache everythingMemory waste, stale dataCache hot data, let cold data expire
No TTLData stale foreverAlways set reasonable TTL
Cache stampedeDB overwhelmed on cache missLock-based or probabilistic early expiry
Caching database queriesHard to invalidateCache at entity/result level
Single cache instanceSingle point of failureRedis Cluster or Sentinel

Caching is not free magic — it is a trade-off between freshness, consistency, and performance. Every cache entry is a promise that the data is probably correct.

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →