Why caching mattersCore idea
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
graph LR
Client["Client"] --> App["Application"]
App -->|"1. check"| Cache["Cache (Redis)<br/>~1 ms"]
App -->|"2. on a miss"| DB[("Database<br/>~100 ms")]
DB -->|"3. fill the cache"| Cache
class Client blue
class App orange
class Cache green
class DB violet
classDef orange fill:#ffd8a8,stroke:#1e1e1e,color:#1e1e1e
classDef blue fill:#a5d8ff,stroke:#1e1e1e,color:#1e1e1e
classDef green fill:#b2f2bb,stroke:#1e1e1e,color:#1e1e1e
classDef red fill:#ffc9c9,stroke:#1e1e1e,color:#1e1e1e
classDef violet fill:#d0bfff,stroke:#1e1e1e,color:#1e1e1e
classDef yellow fill:#ffec99,stroke:#1e1e1e,color:#1e1e1e
classDef grey fill:#e9ecef,stroke:#1e1e1e,color:#1e1e1eLower latency
A Redis read takes about 1ms; a database query about 100ms.
Higher throughput
One cache node handles 100K+ reads/sec; a database handles 1K–10K.
Lower cost
Fewer database queries means smaller database machines and lower cloud bills.
Cache-aside (lazy loading)Pattern
The most common pattern. The application itself reads from and writes to the cache.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
sequenceDiagram
participant App as Application
participant Cache as Redis Cache
participant DB as Database
App->>Cache: GET user:123
alt Cache hit
Cache-->>App: user data
else Cache miss
App->>DB: SELECT * FROM users WHERE id=123
DB-->>App: user data
App->>Cache: SET user:123 (TTL 1 hr)
endfunction getUser(id: string) {
// 1. Check the cache
let user = cache.get(`user:${id}`);
if (user) return user;
// 2. Cache miss: read from the database
user = db.query("SELECT * FROM users WHERE id = ?", [id]);
// 3. Fill the cache with a TTL
cache.set(`user:${id}`, user, { ttl: 3600 });
return user;
}Pros
Only data someone asked for gets cached. If the cache fails, the app still works by going to the database.
Cons
A miss adds latency. After a restart the cache is empty (“cold”), so a flood of reads hits the database.
Write-throughPattern
The application writes to the cache and the database at the same time, so the cache is always up to date.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
sequenceDiagram
participant App as Application
participant Cache as Redis Cache
participant DB as Database
App->>DB: UPDATE users SET name="John" WHERE id=123
App->>Cache: SET user:123 {name: "John"}
Note over Cache,DB: Both writes happen synchronouslyfunction updateUser(id: string, data: UserUpdate) {
db.update("UPDATE users SET ... WHERE id = ?", [id, data]);
cache.set(`user:${id}`, data, { ttl: 3600 });
}Pros
The cache is always consistent, so reads never miss.
Cons
Writes are slower (two of them). Data nobody reads still takes cache memory.
Write-behind (write-back)Pattern
The application writes only to the cache. A background process copies the changes to the database later, which makes writes very fast.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
sequenceDiagram
participant App as Application
participant Cache as Redis Cache
participant Queue as Background queue
participant DB as Database
App->>Cache: SET user:123 {name: "John"}
Cache->>Queue: queue the DB write
Queue->>DB: batch write (async)
Note over Queue,DB: happens in the backgroundfunction updateUser(id: string, data: UserUpdate) {
cache.set(`user:${id}`, data, { ttl: 3600 });
// A background job writes to the database
queue.push({ type: "db-write", id, data });
}Pros
Extremely fast writes, and many updates can be batched into one.
Cons
If the cache crashes before flushing, data is lost. The background processing adds complexity.
Cache invalidationHow it works
Often called the hardest problem in computer science. When data changes, you must remove (invalidate) the cached copy, or users will see stale data.
Time-based (TTL)simple
Each entry expires after a set time. Easy, but data can be stale until it expires.
Event-basedimmediate
Listen to database change events (change data capture, CDC) and delete the cache key right away.
Version-basedno deletes
Put a version in the key, like user:123:v3. When data changes, bump the version; old keys are simply never read again.
The thundering herd problemScale
When a popular key expires, thousands of requests miss at the same moment and all hit the database. The cache was absorbing that traffic; now the database gets all of it at once.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","actorBkg":"#e7f5ff","actorBorder":"#1e1e1e","noteBkgColor":"#fff9db","noteBorderColor":"#1e1e1e"}}}%%
graph LR
E["popular key expires"] --> H["1000s of requests<br/>miss together"]
H --> O["database overloaded"]
O --> F{"fix"}
F --> M["mutex: one request<br/>refills the cache"]
F --> S["stale-while-revalidate:<br/>serve old, refresh async"]
F --> P["pre-warm before<br/>it expires"]
class E yellow
class H,O red
class M,S,P green
classDef orange fill:#ffd8a8,stroke:#1e1e1e,color:#1e1e1e
classDef blue fill:#a5d8ff,stroke:#1e1e1e,color:#1e1e1e
classDef green fill:#b2f2bb,stroke:#1e1e1e,color:#1e1e1e
classDef red fill:#ffc9c9,stroke:#1e1e1e,color:#1e1e1e
classDef violet fill:#d0bfff,stroke:#1e1e1e,color:#1e1e1e
classDef yellow fill:#ffec99,stroke:#1e1e1e,color:#1e1e1e
classDef grey fill:#e9ecef,stroke:#1e1e1e,color:#1e1e1eRedis vs MemcachedCompare
| Feature | Redis | Memcached |
|---|---|---|
| Data structures | Lists, sets, sorted sets, hashes | Simple key-value only |
| Persistence | Yes (RDB + AOF) | No |
| Threading | Single-threaded (event loop) | Multi-threaded |
| Use cases | Leaderboards, sessions, rate limiting, pub/sub | Simple object caching, HTML fragments |
Common interview mistakesPitfalls
SummarySummary
| Strategy | Write path | Read path | Best for |
|---|---|---|---|
| Cache-aside | Write to DB, invalidate cache | Check cache → DB on miss | Most workloads |
| Write-through | Write to both cache and DB | Read from cache | Consistent reads |
| Write-behind | Write to cache, DB later | Read from cache | Write-heavy workloads |
Frequently asked questionsFAQ
What is the difference between cache-aside and write-through?
Cache-aside loads data into the cache only on a read miss and writes to the database, so the cache can go stale and needs invalidation. Write-through updates the cache and database together on every write, keeping them consistent but adding write latency and storing possibly-unread data.
What happens to a cache when the database is updated?
The system must invalidate or refresh the cached copy. Common options are deleting the key on write so the next read repopulates it, setting a short TTL to bound staleness, or versioning keys. The dangerous pattern is updating the cache directly on writes, which drifts from the database.
When does caching hurt rather than help?
Caching helps reads of hot, relatively static data and hurts when data churns constantly, when every read repopulates a cold cache, or when a thundering herd collapses on a missing key. The overhead usually outpaces the win when cache hit rates stay below roughly 80%.

Now explain caching strategies to me out loud
I'll give you a design problem and push on the trade-offs, like a real interviewer.

