Caching strategies define how your application reads from and writes to a cache relative to the source of truth, usually a database. The five patterns you will meet most often are cache-aside, read-through, write-through, write-behind (write-back), and write-around. Each makes a different trade-off between latency, consistency, and complexity, and picking the right one depends mostly on your read/write mix and how stale your data is allowed to be.
What is a caching strategy?
A cache is a fast, usually in-memory store that holds a copy of data that is expensive to fetch or compute. A caching strategy answers three questions:
- Who loads the cache on a miss - the application or the cache itself?
- What happens on a write - does the cache get updated, invalidated, or skipped?
- When does the database see the write - immediately or later?
The answers determine how often users see stale data, how much load reaches your database, and what happens when the cache fails. If you want the fundamentals first, start with the Caching guide.
Read strategies
Cache-aside (lazy loading)
Cache-aside is the most common pattern. The application owns all the logic: check the cache, and on a miss, read the database and populate the cache.
async function getUser(id: string): Promise<User> {
const key = `user:${id}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const user = await db.users.findById(id);
if (user) {
await redis.set(key, JSON.stringify(user), { EX: 300 });
}
return user;
}
async function updateUser(id: string, patch: Partial<User>): Promise<void> {
await db.users.update(id, patch);
await redis.del(`user:${id}`); // invalidate, let the next read reload
}
Pros: only requested data gets cached, the cache can fail without taking down reads (you just hit the database), and it works with any cache product.
Cons: the first read after a miss is slow, and there is a window where concurrent reads and writes can leave stale data in the cache. Deleting on write, rather than setting the new value, narrows that window, and a TTL bounds how long any stale entry survives.
Read-through
Read-through looks the same to the caller, but the cache itself loads missing data from the database through a configured loader. Libraries like in-process caches with loader functions, or managed caching layers, often work this way.
The benefit is that loading logic lives in one place instead of being repeated in every service. The downside is coupling: the cache must know how to talk to your data source, and not every cache product supports it.
Write strategies
Write-through
With write-through, every write goes to the cache and the database synchronously, and the operation succeeds only when both succeed. Reads that follow a write always find fresh data in the cache.
The cost is higher write latency, since each write touches two systems, and cache space spent on data that may never be read. Write-through pairs naturally with read-through for data that is read soon after it is written, such as user profiles or session state.
Write-behind (write-back)
Write-behind writes to the cache immediately and flushes to the database asynchronously, often in batches. Writes are very fast and the database sees far fewer operations, which helps with write-heavy workloads like counters or activity tracking.
The risk is data loss: if the cache node dies before flushing, those writes are gone unless the cache is replicated and persistent. Ordering and retry logic also become your problem. Use it when you can tolerate losing a small window of writes, or when the cache layer offers strong durability guarantees.
Write-around
Write-around writes only to the database and skips the cache. The cache is populated later by reads, typically using cache-aside. This avoids filling the cache with data that is written once and rarely read, such as logs or bulk imports. The trade-off is that a read immediately after a write will miss.
Caching strategies compared
| Strategy | Read on miss | Write path | Consistency | Best for |
|---|---|---|---|---|
| Cache-aside | App loads from DB | App writes DB, invalidates cache | Eventual, bounded by TTL | General-purpose, read-heavy |
| Read-through | Cache loads from DB | Usually combined with write-through | Similar to cache-aside | Centralizing load logic |
| Write-through | Cache (warm) | Cache and DB synchronously | Strong between cache and DB | Read-after-write workloads |
| Write-behind | Cache (warm) | Cache now, DB later | Risk of loss on failure | Write-heavy, loss-tolerant |
| Write-around | App loads from DB | DB only | Fresh DB, cold cache | Write-once, read-rarely data |
Cache invalidation and eviction
Picking a strategy is only half the job. You also need to decide how entries leave the cache.
- TTL (time to live) puts an upper bound on staleness. Short TTLs mean fresher data but more misses.
- Explicit invalidation deletes or updates the key on write. It is precise but easy to get wrong across multiple services. The cache invalidation guide covers the failure modes.
- Eviction policies such as LRU (least recently used) or LFU (least frequently used) decide what to drop when memory fills up. LRU is a sensible default; LFU helps when a stable set of keys is consistently hot.
A common, robust combination is cache-aside with delete-on-write plus a TTL as a safety net.
Common caching problems and fixes
Cache stampede
When a popular key expires, many requests miss at the same moment and all hit the database. Fixes include a per-key lock so only one request rebuilds the value, serving the stale value while refreshing in the background, and adding random jitter to TTLs so keys do not expire together.
Hot keys
A single extremely popular key can overload one cache node. You can replicate the hot key across nodes, add a small in-process cache in front of the distributed cache, or split the key into several copies and pick one at random.
Cache penetration
Requests for keys that do not exist miss the cache every time. Cache a short-lived "not found" marker, or use a Bloom filter to reject keys that definitely do not exist.
Where to put the cache
Caches exist at many layers: the browser, a CDN, an API gateway, an in-process memory cache, and a distributed cache such as Redis or Memcached. Closer to the user means lower latency but harder invalidation. Most production systems use two or three layers, each with its own TTL.
How to choose a caching strategy
Work through these questions in order:
- Is the workload read-heavy? Start with cache-aside.
- Do users need to read their own writes immediately? Add write-through, or update the cache on write.
- Is the workload write-heavy and tolerant of small losses? Consider write-behind.
- Is most written data rarely read? Use write-around.
- How stale can data be? Set TTLs accordingly.
In a system design interview, naming the strategy and its failure mode is usually more valuable than naming the cache product.
Key takeaways
- Cache-aside is the default: simple, resilient to cache failures, and works with any store.
- Write-through keeps the cache fresh at the cost of slower writes.
- Write-behind makes writes fast but risks losing data if the cache fails before flushing.
- Write-around avoids polluting the cache with data that is rarely read.
- TTLs, jitter, and stampede protection matter as much as the strategy itself.
Frequently asked questions
What is the most common caching strategy?
Cache-aside, also called lazy loading, is the most widely used. The application checks the cache, loads from the database on a miss, and invalidates the cache on writes. It is simple and degrades gracefully when the cache is unavailable.
What is the difference between write-through and write-behind?
Write-through updates the cache and the database synchronously, so both are consistent when the write returns. Write-behind updates only the cache immediately and writes to the database later. Write-behind is faster but can lose data if the cache fails before flushing.
Should I update or delete the cache on write?
Deleting is usually safer. Updating the cache from multiple concurrent writers can leave an older value in place if writes land out of order. Deleting forces the next read to load the latest value from the database.
How do I prevent a cache stampede?
Use a lock or single-flight mechanism so only one request rebuilds an expired key, add random jitter to TTLs, and consider serving slightly stale data while refreshing in the background.