Skip to main content
Articles/System Design/caching-strategies-redis-memcached
Scenario Article

How to serve data at lightning speed | Caching Strategies (Redis/Memcached)

The Performance Problem: Database Bottlenecks

Abstract Algorithms•Jul 9, 2026
💡 Executive Summary

Caching accelerates application performance by storing frequently accessed data in fast, in-memory key-value stores like Redis or Memcached.

🎯 System Scenario & Requirements

System Goal

Caching accelerates application performance by storing frequently accessed data in fast, in-memory key-value stores like Redis or Memcached.

Actors & Components

  • Application Workers
  • Volatile Memory Tier
  • Persistent Disk Tier
  • Worker Process
  • In-Memory Cache
  • Primary Database

Functional Requirements

  • TLDR: Caching accelerates application performance by storing frequently accessed data in fast, in-memory key-value stores like Redis or Memcached.
  • Selecting the correct caching strategy—such as Cache-Aside, Write-Through, or Write-Behind—determines how data consistency is maintained between the cache and the primary database.
  • In modern web development, database access is often the primary bottleneck.

Constraints & Assumptions

  • Constraints are derived from the rebuilt scenario stages.

⚡ Execution Stages & Decision Trace

1

Stage 1 — Caching Patterns

Action / Decision:

The flow of data between the application, the cache, and the database is governed by specific caching patterns.

Component Behavior:

Cache-Aside (Lazy Loading) This is the most common caching pattern.

Observable Output: The application is responsible for managing both the cache and the database.
Read Full Stage in Deep Dive ↓
📖

Deep Dive

Detailed Architectural Analysis

Scenario context

Caching accelerates application performance by storing frequently accessed data in fast, in-memory key-value stores like Redis or Memcached. Selecting the correct caching strategy—such as Cache-Aside, Write-Through, or Write-Behind—determines how data consistency is maintained between the cache and the primary database.

Observable outcome: - Read Flow: The application checks the cache.

Stage 2 — Eviction Policies and TTL

  • Memory (RAM) is expensive and limited.
  • A cache cannot grow indefinitely.
  • To manage memory constraints, caches use two mechanisms: Time-To-Live (TTL) and Eviction Policies .
  • Time-To-Live (TTL) Every cached key should be assigned an expiration timer (TTL).

Observable outcome: Time-To-Live (TTL) Every cached key should be assigned an expiration timer (TTL).

Stage 3 — Caching Gotchas and Mitigation

  • Cache Avalanche - The Problem: A large number of cached keys expire at the exact same time, or the cache server crashes.
  • This causes all client traffic to hit the database simultaneously, causing a database crash.
    • Mitigation: Add a random jitter (e.g., adding a random offset of 1 to 5 minutes) to the TTL of every key, spreading expiration events evenly over time.
  • Cache Penetration - The Problem: Clients request keys that do not exist in either the cache or the database (e.g., searching for a non-existent user ID).

Observable outcome: Cache Penetration - The Problem: Clients request keys that do not exist in either the cache or the database (e.g., searching for a non-existent user ID).

Stage 4 — Write-Around: Keep One-Time Writes Out of the Cache

  • Write-around sends writes directly to the database and invalidates any old cached value.
  • The cache is populated only if a later read needs the record.
  • It is useful for imports, audit events, large objects, and other data that is written once but may never become hot.
  • Its cost is predictable: the first read after every write is a cache miss.

Observable outcome: Its cost is predictable: the first read after every write is a cache miss.

Stage 5 — Practical Cache-Aside with Stampede Protection

  • ~~~ts async function getProduct(id: string) { const key = 'product:v3:' + id; const cached = await redis.get(key); if (cached) return JSON.parse(cached); // SET NX lets one request rebuild the key.
  • Others briefly retry or serve stale data.
  • const lockKey = key + ':lock'; const ownsLock = await redis.set(lockKey, requestId, { NX: true, PX: 3 000 }); if (!ownsLock) return retryWithJitter(() = getProduct(id), 40, 120); try { const product = await db.products.…
  • ttlSeconds : 30 }); return product; } finally { // Production code should compare the lock token before deletion.

Observable outcome: ttlSeconds : 30 }); return product; } finally { // Production code should compare the lock token before deletion.

Stage 6 — Capacity Planning and Observability

  • Suppose an API serves 20,000 reads per second, the database safely handles 2,500, and the target cache hit ratio is 90%.
  • The expected database read rate is 2,000 per second: within budget, but with little room for a stampede.
  • At 80% hits the database receives 4,000 reads per second and is already overloaded.
  • Capacity planning must test degraded hit ratios, not only the happy path.

Observable outcome: Capacity planning must test degraded hit ratios, not only the happy path.

End-to-end outcome

After working through the stages, you should be able to explain:

    • A cache reduces load only while its miss path remains controlled.
    • Pattern choice is primarily a consistency and failure-semantics decision.

Alternatives considered

Decision Guide: Selecting the Caching Pattern

Use this guide to choose the caching strategy for your system: - Use Cache-Aside if: - Your application is read-heavy and you want to cache data dynamically.

Redis vs Memcached: Choose Capabilities, Not Brand Recognition

Choose Memcached when values are disposable blobs, operations are simple, and rebuilding the entire cache is acceptable.

Failure and degraded modes

The source does not isolate a separate failure mode. Limits that matter for the scenario are called out in the stage decisions above.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit