Definition
- Cache-aside (lazy loading) : on a read, the application checks the cache; on a miss it loads from the database and populates the cache, so only requested data is cached.
- Write-through : writes to the cache and the database together on every write, so the cache is always consistent with the database.
- Write-behind (write-back) : writes to the cache immediately and queues the database write to happen asynchronously, trading durability for speed.
- Cache invalidation : keeping cached entries fresh via time-to-live (TTL) or event-based deletion when the underlying data changes.
- Cache stampede (thundering herd) : when a popular key expires and many requests miss the cache simultaneously and all slam the database; prevented with a single-flight lock or probabilistic early expiration.
The teller at National Bank of India keeps a note of Rajesh's balance on the desk so they do not walk to the vault every time. Cache-aside means the teller fetches from the vault only when the note is missing. Write-through means whenever the balance changes, the teller updates both the note and the vault book together. Write-behind means the teller updates the note instantly and writes to the vault book later. Invalidation means tearing up an outdated note. Stampede prevention means that when many tellers need the same missing note at once, only one walks to the vault while the others wait for the answer.
Understanding the Concepts
What is cache-aside (lazy loading)?
Cache-aside, also called lazy loading, is the most common read strategy. On a read, the application checks the cache. On a miss it loads from the database and populates the cache, so only requested data is cached. Its weakness is that the first read is slow and, without invalidation, the cache can go stale after a database write.
package com.bank.cache;
import java.math.BigDecimal;
public class CacheAsideBalance {
public BigDecimal getBalance(String accountId) {
BigDecimal cached = cache.get(accountId);
if (cached != null) return cached; // cache HIT
BigDecimal balance = database.loadBalance(accountId); // MISS -> DB
cache.put(accountId, balance); // populate cache
return balance;
}
}
What is write-through caching?
Write-through writes to the cache and the database together on every write, so the cache is always consistent with the database. Writes are slightly slower because two systems are updated, but reads never see stale data. This is my default for balances in banking, where correctness beats a little write latency.
package com.bank.cache;
import java.math.BigDecimal;
public class WriteThroughBalance {
public void updateBalance(String accountId, BigDecimal newBalance) {
database.save(accountId, newBalance); // durable first
cache.put(accountId, newBalance); // then cache (consistent)
}
}
What is write-behind (write-back) caching?
Write-behind writes to the cache immediately and queues the database write to happen asynchronously. Writes are very fast and can be batched, but if the cache node crashes before flushing, recent writes are lost. This makes write-behind unsuitable for money and acceptable only for tolerant data like analytics counters or view counts.
package com.bank.cache;
public class WriteBehindCounter {
public void incrementViewCount(String pageId) {
cache.increment(pageId); // fast, in-memory
asyncFlushQueue.enqueue(pageId); // DB write happens later
// If cache crashes before flush, recent counts are lost
}
}
How does cache invalidation work?
Invalidation keeps the cache correct. A time-to-live lets entries expire automatically, so stale data self-heals after a bounded delay. This is simple but allows brief staleness. Event-based invalidation deletes or updates the cache entry the moment the underlying data changes, which is precise but requires wiring the write path to the cache.
package com.bank.cache;
public class InvalidationStrategies {
// TTL-based: entry expires after 60 seconds
public void cacheTtl(String key, String value) {
cache.put(key, value, Duration.ofSeconds(60));
}
// Event-based: invalidate immediately on data change
public void onBalanceChanged(String accountId) {
cache.delete(accountId); // next read will reload from DB
}
}
What is cache stampede and how do you prevent it?
Cache stampede, also called the thundering herd, happens when a very popular key expires and many requests simultaneously miss the cache and all hit the database at once. Prevention techniques include a single-flight lock so only one request rebuilds the value while others wait, and staggered or probabilistic early expiration so the key is refreshed before it fully expires.
package com.bank.cache;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class StampedeProtection {
private final Lock rebuildLock = new ReentrantLock();
public String getWithProtection(String key) {
String value = cache.get(key);
if (value != null) return value;
if (rebuildLock.tryLock()) { // only one thread rebuilds
try {
value = cache.get(key); // double-check after lock
if (value != null) return value;
value = database.load(key); // one DB call, not thousands
cache.put(key, value);
return value;
} finally {
rebuildLock.unlock();
}
}
// Other threads wait briefly and retry
return waitAndRetry(key);
}
}
How do you compare these strategies?
| Strategy | Read speed | Write speed | Consistency | Durability risk |
|---|---|---|---|---|
| Cache-aside | Fast (hit) / Slow (miss) | N/A (read-only strategy) | Can go stale | None |
| Write-through | Always fast | Slower (two writes) | Always fresh | None |
| Write-behind | Always fast | Very fast | Always fresh | Data loss on crash |
package com.bank.cache;
public class StrategyComparison {
public String recommend(String dataType) {
return switch (dataType) {
case "account_balance" -> "Write-through + event invalidation (correctness)";
case "analytics_counter" -> "Write-behind + TTL (speed, loss OK)";
case "product_catalog" -> "Cache-aside + short TTL (read-heavy, rarely changes)";
default -> "Cache-aside + TTL (safe default)";
};
}
}
When should you use which strategy in banking?
For banking balances and ledger data, use write-through with event-based invalidation. The cache is always consistent, and any balance change is immediately reflected. For analytics counters and non-critical metrics, write-behind is fine because losing a few seconds of counts on a crash is acceptable. For product catalogs and reference data that change rarely, cache-aside with a short TTL is simple and effective.
What should a senior engineer know?
A senior engineer knows that these strategies combine. Cache-aside reads with write-through or event-based invalidation and stampede protection on the hottest keys gives both speed and correctness. They also know that cache consistency is harder in distributed systems where multiple application instances can race to update the same cache key, and that techniques like cache-aside with delete-on-write (rather than update-on-write) reduce the window for race conditions.
Visual Overview
Cache-Aside (Lazy Load) Write-Through
┌────────┐ 1. GET ┌───────┐ ┌────────┐ 1. WRITE ┌───────┐
│ App │─────────►│ Cache │ │ App │──────────►│ Cache │
└────┬───┘ 2. MISS └───┬───┘ └────┬───┘ └───┬───┘
│ │ │ 2. WRITE │
│ 3. LOAD │ │ │
▼ │ ▼ │
┌────────┐ │ ┌────────┐ │
│ DB │───────────────┘ │ DB │──────────────┘
└────────┘ 4. POPULATE cache └────────┘ Both updated together
Write-Behind Stampede Prevention
┌────────┐ 1. WRITE ┌───────┐ ┌──────┐ MISS ┌───────┐ MISS
│ App │──────────►│ Cache │ │ Req1 │──────►│ Cache │◄──── Req2
└────────┘ └───┬───┘ └──┬───┘ └───────┘ Req3
│ │ gets lock wait
async │ ▼
│ ┌────────┐
▼ │ DB │ only ONE DB call
┌────────┐ └────┬───┘
│ DB │ │ populate cache
└────────┘ ▼
(may lose data all requests served
on crash) from fresh cache)
Key Takeaway
Caching strategies define how an application reads from and writes to a cache relative to its backing database, and I pick them per data type rather than applying one everywhere. For reads, cache-aside, or lazy loading, is my default: on a read I check the cache, and on a miss I load from the database and populate the cache, so only requested data is cached. Its weakness is that the first read is slow and the cache can go stale after a write, so I always pair it with an invalidation strategy.
For writes, I choose between write-through and write-behind by how much I value correctness versus latency. Write-through writes to the cache and database together, so the cache is never stale, at the cost of a slightly slower write; this is my default for bank balances, where a wrong value is unacceptable. Write-behind writes to the cache immediately and flushes to the database asynchronously, which is very fast and batchable but loses recent writes if the cache crashes before flushing, so I use it only for tolerant data like analytics counters, never for money.
Invalidation keeps the cache honest. A time-to-live expires entries automatically so staleness self-heals after a bounded delay, which is simple but allows brief staleness. Event-based invalidation deletes or updates the entry the instant the underlying data changes, which is precise and what I use for balances so that right after a transfer at National Bank of India, the next read reflects the new balance rather than the old one.
The subtle failure I always design for is the thundering herd, or cache stampede. When a very popular key like the current interest rate expires, a flood of requests can miss simultaneously and all slam the database at once. I prevent this with a single-flight lock so only one request rebuilds the value while the others wait, and I complement it with staggered or probabilistic early expiration so hot keys refresh just before they fully expire.
Remember one sentence: pick caching strategies per data type : write-through for money, cache-aside for reads, stampede protection for hot keys, and never write-behind for anything you cannot afford to lose.
---