HLDSystem Design25 min read

Caching in System Design

Learn how caching works from first principles to production-scale systems: cache-aside, read-through, write-through, eviction, invalidation, stampedes, hot keys, Redis, distributed locking and high-traffic architecture.

Fundamentals

What is a Cache?

A cache is a faster storage layer that temporarily keeps frequently accessed or expensive-to-compute data close to the consumer.

Imagine your application needs a user profile. Reading that profile from a distributed database may take significantly longer than reading it from an in-memory cache.

Without Cache

Every request reaches the database or downstream service.

❌ Higher latency

❌ More database load

❌ Harder to absorb traffic spikes

With Cache

Frequently used values can be served from a faster layer.

✅ Lower latency

✅ Less database pressure

✅ Better burst handling

Caching does not make the database unnecessary

The persistent database should usually remain the source of truth. Cache is an optimization layer, not a replacement for durable storage.

Architecture

Where Can We Cache?

Caching can exist at several layers. Large systems often combine multiple cache layers rather than relying on one Redis cluster.

User

Browser / Mobile App

Request

Browser / CDN Cache

Static assets, images, public content

Application Servers

Local in-memory cache

Distributed Cache

Redis / Memcached

Cache Miss

Persistent Database

Source of truth

Browser Cache

CSS, JavaScript, fonts, API responses and other client-cacheable resources.

CDN / Edge Cache

Static and public content cached geographically close to users.

Local Application Cache

Per-instance in-memory cache with extremely low latency.

Distributed Cache

A shared cache such as Redis accessible across application instances.

Database Cache

Buffer pools, query caches and database-managed memory optimizations.

Computed Result Cache

Store expensive calculations, search results, recommendations or rendered responses.

Core Interview Topic

Caching Strategies

An interviewer usually expects you to explain not only that you will use Redis, but how reads and writes interact with the cache.

1

Cache Aside

Lazy Loading

Read

Application checks cache first. On miss, it reads the database and writes the result into cache.

Write

Application writes directly to the database and usually invalidates the cached key.

Good for: Read-heavy systems where not every database row needs to be cached.

Trade-off: First request after a miss is slower. Stale data is possible if invalidation fails.

2

Read Through

Cache-managed reads

Read

Application asks the cache. The cache itself loads missing data from the underlying store.

Write

Depends on the cache implementation and write strategy.

Good for: Systems wanting caching logic abstracted away from application code.

Trade-off: Needs cache/library support for loading data from the source.

3

Write Through

Synchronous write

Read

Reads usually come from cache.

Write

Every write updates both the cache and persistent database before success is returned.

Good for: Systems where cache and database consistency are important.

Trade-off: Higher write latency because two systems must be updated.

4

Write Back

Write Behind

Read

Usually served from cache.

Write

Write reaches cache first. Database persistence happens asynchronously.

Good for: High write throughput where small persistence delays are acceptable.

Trade-off: Data can be lost if the cache fails before the asynchronous database write.

5

Refresh Ahead

Proactive refresh

Read

Frequently accessed data is refreshed before its TTL expires.

Write

Usually combined with another strategy such as cache-aside.

Good for: Very hot predictable keys where expiration misses would cause spikes.

Trade-off: Can refresh data that nobody ends up requesting.

Cache-Aside Flow

Client

API

Redis

GET user:42

CACHE MISS

Database

Fetch user → populate Redis → return

Cache Aside Pseudocode
async function getUser(userId) {
    const key = `user:${userId}`;

    const cached = await redis.get(key);

    if (cached) {
        return JSON.parse(cached);
    }

    const user = await database.getUser(userId);

    if (!user) {
        return null;
    }

    await redis.set(
        key,
        JSON.stringify(user),
        "EX",
        300
    );

    return user;
}

Memory Management

Cache Eviction Policies

Cache memory is finite. When it fills up, the system must decide what data to remove.

PolicyMeaningUseful When
LRULeast Recently UsedRecent access predicts future access.
LFULeast Frequently UsedFrequently accessed objects should remain cached.
FIFOFirst In First OutSimple eviction is enough.
TTLTime To LiveData becomes stale after a known period.
RandomRandom evictionVery simple and low-overhead policy is acceptable.

TTL and eviction are different

TTL removes data because it has become too old. Eviction removes data because memory is under pressure. A production cache may use both.

The Hard Part

Cache Invalidation

Caching is easy when data never changes. The difficult part is keeping cached data acceptably fresh after writes.

Delete on Write

Update the database, then delete the related cache entry. The next read repopulates it.

Update Cache

Update the database and immediately update the cached copy.

TTL Expiry

Allow stale data for a bounded time and let the cache expire naturally.

Update + Invalidate
async function updateUser(userId, payload) {

    await database.updateUser(
        userId,
        payload
    );

    // Remove potentially stale data.
    await redis.del(
        `user:${userId}`
    );

    return { success: true };
}

What if DB update succeeds but cache delete fails?

Now the database contains the new value while Redis may still contain the old value. Possible approaches include short TTLs, retry queues, change-data-capture, event-driven invalidation and versioned cache keys.

High Traffic

Cache Stampede / Thundering Herd

A cache stampede happens when a hot key expires and a large number of requests simultaneously discover the cache miss.

Req 1

Req 2

Req 3

Req 4

Req 5

Redis

hot:key expired → MISS

All requests now fall through

Database

Suddenly receives thousands of identical expensive queries

Ways to Stop a Stampede

Single Flight / Mutex

Only one request rebuilds the value. Other requests wait for that result.

TTL Jitter

Add random variation to expiry times so thousands of keys do not expire simultaneously.

Stale While Revalidate

Serve a slightly stale value while one background worker refreshes it.

Refresh Ahead

Refresh a highly popular key before the TTL actually expires.

Request Coalescing

Merge identical concurrent cache-miss requests into one backend operation.

Rate Limit / Queue

Protect the database by controlling how much miss traffic is allowed through.

TTL Jitter
const BASE_TTL = 300;

// Random additional expiry time.
// Keys will not all expire together.
const jitter =
    Math.floor(Math.random() * 60);

await redis.set(
    key,
    value,
    "EX",
    BASE_TTL + jitter
);

Hot Key Problem

The Celebrity Problem

Imagine a celebrity publishes a post and tens of millions of users request the same profile or post. One cache key can become far hotter than everything else.

User

User

User

User

Millions request same key

celebrity:123

One extremely hot Redis key / shard

Local L1 Cache

Cache the hot object inside application instances so many requests never reach Redis.

Replication

Spread read traffic across replicas when the data can be safely read from multiple nodes.

Key Replication

For extreme read hotspots, intentionally maintain multiple copies such as hot:123:0, hot:123:1, etc.

CDN / Edge

For public content, move requests closer to users and keep them away from the origin entirely.

Longer TTL

If freshness requirements allow it, avoid repeatedly rebuilding extremely popular content.

Pre-Warming

Populate known hot data before a predictable event or traffic surge.

Scale

How Do We Handle Millions of Requests?

One Redis node is not the architecture. At large scale we reduce traffic at every possible layer before it reaches the primary database.

Millions of Users

Web / Mobile

CDN / Edge

Absorb static + cacheable traffic

Load Balancer

Spread traffic

API 1

L1 cache

API 2

L1 cache

API N

L1 cache

Redis Cluster

Distributed L2 cache

Only misses

Database Cluster

Primary + replicas / partitions

The goal is traffic reduction

If 95% of requests can be served before reaching the database, a million incoming requests do not necessarily mean a million database queries.

Distributed Cache

Designing Redis Properly

Redis is fast, but poor key design, unlimited TTLs or one hot shard can still create serious production problems.

Key Naming

Redis Keys
user:42
user:42:profile
product:1001
movie:778:show:992
show:992:seat:A10
show:992:seat:A10:lock
feed:user:42:v3

Use Namespaces

Keys such as user:42 and product:42 avoid collisions and are easier to inspect.

Always Think About TTL

Permanent cache entries often become stale or consume memory indefinitely.

Avoid Giant Values

Huge serialized objects increase network, CPU and memory cost.

Monitor Hit Ratio

A cache with a very low hit ratio may add complexity without meaningful benefit.

Watch Hot Keys

One disproportionately popular key can overload a shard even when total cluster capacity looks healthy.

Plan Failure Mode

Ask what your application does when Redis is unavailable.

System Design Case Study

BookMyShow-Style Seat Locking with Redis

Let's apply caching and distributed locking concepts to a high-concurrency ticket booking system.

Important

This is an interview-style architecture for a BookMyShow-like ticketing platform. It illustrates how Redis can be used for temporary seat holds; it is not a claim about BookMyShow's private production code.

Seat State Machine

AVAILABLE

Seat can be selected

HELD

Temporary Redis lock

BOOKED

Persisted after successful booking

If payment fails, checkout is abandoned or the hold expires: HELD → AVAILABLE

Booking Flow

User selects A10

Seat Reservation API

Validate show + seat

Atomic Redis Lock

SET seat:992:A10:lock token NX PX 300000

Lock acquired

5 Minute Seat Hold

Other users see seat as unavailable

Payment

Idempotent booking flow

Success

Persistent DB

Atomically mark seat BOOKED

Acquire Temporary Seat Hold
const lockKey =
    `show:${showId}:seat:${seatId}:lock`;

const token = crypto.randomUUID();

const acquired = await redis.set(
    lockKey,
    token,
    "NX",
    "PX",
    5 * 60 * 1000
);

if (!acquired) {
    throw new Error(
        "Seat already held by another user"
    );
}

// Continue checkout...

Why Store a Unique Token?

Imagine User A obtains a lock. The lock expires. Then User B obtains a new lock for the same seat. User A must not accidentally delete User B's lock during cleanup.

Safe Redis Unlock
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

What Happens When 100 People Click A10?

U1

U2

U3

U4

U100

Same seat

Redis Atomic SET NX

Only one request creates the lock

Winner

One request acquires the seat hold and proceeds to checkout.

Everyone Else

Redis refuses the `NX` operation and the API returns seat unavailable.

But Redis Must Not Be Your Only Protection

The cache lock protects the high-concurrency checkout experience, but the final booking should still be protected by the persistent data layer.

Final DB Guard
UPDATE seats

SET
    status = 'BOOKED',
    booking_id = :bookingId

WHERE
    show_id = :showId
    AND seat_id = :seatId
    AND status = 'AVAILABLE';

Defense in depth

Redis gives you a fast temporary hold. The database gives you durable correctness. Idempotency protects retries. TTL protects abandoned checkouts.

Payment Success vs Failure

Payment Success

1. Verify payment

2. Idempotently create booking

3. Persist seat as BOOKED

4. Remove temporary lock

5. Publish booking event

Payment Failure

1. Mark payment attempt failed

2. Safely release the Redis lock

3. Or allow TTL to expire

4. Seat becomes available again

5. Notify active clients

Production Engineering

Caching Failure Scenarios

A strong system design answer explains what happens when the happy path fails.

Redis Goes Down

Fail open or fail closed depending on the use case. Normal profile reads may temporarily fall back to the DB with strict rate limiting. Seat locks require much stricter correctness handling.

Cache and DB Disagree

Treat the database as authoritative. Use TTL, invalidation retries, versioning or events to repair stale cache state.

All Keys Expire Together

Use TTL jitter, proactive refresh and staggered warm-up.

One Key Becomes Extremely Hot

Use local cache, replicas, CDN, key replication or precomputed responses depending on the workload.

Redis Memory Is Full

Configure appropriate max-memory policy, TTLs and alerts. Monitor eviction rate and memory fragmentation.

Network Partition

Define whether stale cached reads are acceptable and avoid assuming every distributed lock is an absolute correctness guarantee.

Important Metrics

Hit Ratio
Miss Ratio
P50 / P95 / P99
Memory Usage
Evictions
Hot Keys
Connection Count
CPU
Network I/O

Trade-offs

When Should You Not Cache?

Adding Redis to every system is not automatically good system design.

×

Data changes extremely frequently and stale reads are unacceptable.

×

The underlying database query is already cheap and traffic is low.

×

The expected cache hit ratio is extremely small.

×

The additional operational complexity is greater than the performance benefit.

×

The value is highly sensitive and should not be duplicated without a clear security model.

×

Strong transactional consistency is required on every read.

Interview Preparation

Caching Questions You Should Be Able to Answer

If you can explain these clearly, including trade-offs, you are in a strong position for most HLD interviews.

1

What problem does caching solve?

2

What is the difference between cache-aside and read-through?

3

When would you choose write-through over write-back?

4

What is a cache stampede?

5

How would you prevent a thundering herd?

6

What is TTL jitter?

7

What is the celebrity / hot-key problem?

8

How do you invalidate cache after a database update?

9

What happens if the database update succeeds but cache invalidation fails?

10

What happens if Redis is unavailable?

11

How do you cache data across multiple application servers?

12

What is the difference between an L1 local cache and an L2 distributed cache?

13

LRU vs LFU — when would you use each?

14

How would you design a cache for millions of requests per second?

15

How would you prevent double booking using Redis?

16

Why should the database still protect final booking correctness?

17

How do idempotency and caching interact in a payment or booking system?

18

What metrics would you monitor for Redis?

Quick Revision

Frequently Asked Questions

Short answers to some of the most common caching questions.

What is caching in system design?

Caching stores frequently used data in a faster storage layer so future requests can avoid repeatedly accessing a slower database, service or remote resource.

What is a cache stampede?

A cache stampede happens when a popular cached value expires and many requests simultaneously miss the cache and hit the underlying database or service.

What is the celebrity or hot-key problem?

The celebrity problem occurs when one key receives disproportionately high traffic, causing a single cache shard, node or backend dependency to become overloaded.

Why is Redis commonly used for caching?

Redis provides fast in-memory access, TTL support, atomic commands, multiple data structures and clustering capabilities, making it useful for caching and short-lived distributed state.

Can Redis prevent double booking?

Redis can be used for short-lived atomic seat holds, but the persistent database should remain the source of truth and the booking flow should also use idempotency and database-level consistency protections.

TargetTrek HLD

Want to master complete System Design?

Go beyond caching. Learn requirements, APIs, databases, scaling, queues, locking, concurrency, failure handling and complete interview-style designs such as BookMyShow and other large-scale systems.

CachingDatabasesRate LimitingQueuesConcurrencyBookMyShowUberScaling

Continue Learning