Caching Strategies

← Back to System Design 101 | ← Previous: Scalability Patterns

Why Caching Matters

Caching is one of the most effective ways to improve system performance. A well-implemented cache can reduce database load by 80-90%, decrease API response times from seconds to milliseconds, and save significant infrastructure costs.

I've seen caching transform struggling systems into performant ones. But I've also seen poorly implemented caching cause subtle bugs and stale data issues. The key is understanding when to cache, what to cache, and how to invalidate cached data.

Caching Fundamentals

What to Cache

Good cache candidates (from my experience):

  • Data that's read frequently but changes rarely (product catalogs, user profiles)

  • Expensive computations (aggregations, reports)

  • External API responses (third-party data)

  • Session data and authentication tokens

  • Static content (images, CSS, JavaScript)

Poor cache candidates:

  • Data that changes constantly (real-time stock prices, live sports scores)

  • User-specific data that's rarely reused

  • Data that must always be current (financial transactions)

  • Large objects that exceed cache memory limits

Cache Hit Ratio

The percentage of requests served from cache vs database.

My target metrics:

  • Cache hit ratio > 80% for frequently accessed data

  • Cache hit ratio > 95% for static content

  • If hit ratio < 70%, I reconsider the caching strategy

Caching Patterns

1. Cache-Aside (Lazy Loading)

The application manages the cache directly. Most common pattern I use.

How it works:

  1. Check cache first

  2. If miss, fetch from database

  3. Store in cache

  4. Return data

When I use cache-aside:

  • Read-heavy workloads

  • When cache misses are acceptable

  • When I need fine control over caching logic

Challenges I've faced:

  • Cache stampede (multiple requests hit DB when cache expires)

  • Inconsistency between cache and database

  • Complex invalidation logic

2. Write-Through Cache

Data is written to cache and database simultaneously.

When I use write-through:

  • When data consistency is critical

  • When read/write ratio is high

  • When I can tolerate slightly slower writes

Trade-offs:

  • βœ… Cache always synchronized with database

  • βœ… No cache misses for written data

  • ❌ Higher write latency

  • ❌ Wasted cache space if data is never read

3. Write-Back (Write-Behind) Cache

Data is written to cache first, then asynchronously written to database.

When I use write-back (rarely):

  • High-write workloads where latency is critical

  • Analytics and counters where eventual consistency is acceptable

  • When I have reliable cache infrastructure with persistence

⚠️ Risks:

  • Data loss if cache fails before writing to database

  • Complex error handling

  • Harder to debug issues

4. Refresh-Ahead Cache

Proactively refresh cache before expiration.

When I use refresh-ahead:

  • Expensive computations that are frequently accessed

  • Dashboards and analytics

  • When avoiding cache misses is critical

Cache Invalidation

Phil Karlton famously said: "There are only two hard things in Computer Science: cache invalidation and naming things." He was right.

Time-Based Expiration (TTL)

The simplest invalidation strategy.

Event-Based Invalidation

Invalidate cache when data changes.

Cache Tags

Group related cache entries for easier invalidation.

CDN Caching

Content Delivery Networks cache static assets close to users.

CDN Configuration

Cache-Control Headers

Redis Best Practices

Redis is my go-to caching solution. Here are patterns I use:

Memory Management

Lessons Learned

What worked:

  1. Start with cache-aside pattern - simplest and most flexible

  2. Use Redis for almost everything - it's fast, reliable, and well-supported

  3. Set conservative TTLs initially, then optimize based on metrics

  4. Tag-based invalidation for complex scenarios

  5. Monitor cache hit ratio religiously

What didn't work:

  1. Caching everything without measuring benefit

  2. Very long TTLs without invalidation strategy

  3. Write-back caching without proper backup mechanisms

  4. Sharing Redis instance between different use cases (sessions, cache, queues)

  5. Not setting max memory limits - caused OOM issues

My caching checklist:

  • βœ… Defined clear cache keys with namespace

  • βœ… Set appropriate TTLs

  • βœ… Implemented invalidation strategy

  • βœ… Added cache metrics monitoring

  • βœ… Handled cache failures gracefully

  • βœ… Documented caching decisions

What's Next

With caching strategies in place, let's explore database design for distributed systems:


Navigation:

Last updated