Cache-Aside Pattern: The Most Common Caching Strategy Explained
In this tutorial, you will learn about Cache. We cover key concepts, practical examples, and best practices to help you master this topic.
The cache-aside pattern (also called lazy loading) is the most widely used Caching Strategy. The application code explicitly checks the cache before fetching data from the source. On a cache miss, it loads the data from the source, stores it in the cache, and returns it. On a cache hit, it returns the cached data directly.
flowchart LR
A[Request] --> B[Check Cache]
B -->|Cache Hit| C[Return Cached Data]
B -->|Cache Miss| D[Fetch from Database]
D --> E[Store in Cache]
E --> F[Return Data]
What You'll Learn
- Cache-aside pattern implementation and best practices
- Handling cache miss concurrency (thundering herd prevention)
- Cache-aside with write-through for writes
- Common pitfalls and optimizations
Why It Matters
Cache-aside is the foundation for most caching implementations. It is simple to implement, works with any cache backend, and allows fine-grained control over what gets cached and for how long. Mastering it is essential before exploring more complex patterns.
Real-World Use
A payment processing API uses cache-aside to cache merchant configuration data. Configuration is read on every Transaction but rarely changes. Cache hit rate is 99.5%, reducing database queries from 10,000/second to 50/second.
Cache-Aside Implementations
Basic Cache-Aside
async function getProduct(id) {
const cacheKey = `product:${id}`;
const cached = await cache.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const [rows] = await db.query('SELECT * FROM products WHERE id = ?', [id]);
if (rows.length > 0) {
await cache.setEx(cacheKey, 3600, JSON.stringify(rows[0]));
}
return rows[0];
}
Expected output:
First call: cache miss, DB query, cache set, return. Second call within 3600s: cache hit, return instantly.
Cache-Aside with Thundering Herd Protection
const inFlight = new Map();
async function getProductSafe(id) {
const cacheKey = `product:${id}`;
const cached = await cache.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Check if another request is already fetching this key
const existing = inFlight.get(cacheKey);
if (existing) {
return existing;
}
const promise = (async () => {
try {
const [rows] = await db.query('SELECT * FROM products WHERE id = ?', [id]);
const data = rows[0] || null;
if (data) {
await cache.setEx(cacheKey, 3600, JSON.stringify(data));
}
return data;
} finally {
inFlight.delete(cacheKey);
}
})();
inFlight.set(cacheKey, promise);
return promise;
}
Expected output:
When 100 concurrent requests arrive for a missing key, only one DB query executes. All 100 await the same promise.
Cache-Aside with Batch Loading
async function getProducts(ids) {
const uncached = [];
const results = [];
for (const id of ids) {
const cached = await cache.get(`product:${id}`);
if (cached) {
results.push(JSON.parse(cached));
} else {
uncached.push(id);
}
}
if (uncached.length > 0) {
const placeholders = uncached.map(() => '?').join(',');
const [rows] = await db.query(
`SELECT * FROM products WHERE id IN (${placeholders})`,
uncached
);
const multi = cache.multi();
for (const row of rows) {
multi.setEx(`product:${row.id}`, 3600, JSON.stringify(row));
results.push(row);
}
await multi.exec();
}
return results;
}
Expected output:
Uncached IDs are fetched in a batch DB query. All fetched rows are cached simultaneously using Redis pipelining.
Common Mistakes
- Not handling null values — if a key does not exist in the database, cache-aside should cache a null sentinel to avoid repeated DB lookups.
- Caching mutable objects by reference — if the caller modifies the returned object, the cache is corrupted. Always return a copy or serialize/deserialize.
- Setting the same TTL for all entries regardless of access frequency — hot keys should have longer TTLs.
- Not using multi-get/multi-set when loading batches — N individual cache calls add significant latency.
- Ignoring cache stampede on cache miss — without coalescing, a popular key that expires will cause a thundering herd.
Practice Questions
- In cache-aside, when is the cache populated?
- How does cache-aside handle data updates without write-through?
- What is the thundering herd problem in cache-aside?
- Why should you cache null values from the database?
- How does batch loading improve cache-aside performance?
Challenge
Implement a cache-aside layer for a user profile API that handles 10,000 requests/second. Use thundering herd prevention, batch loading for list endpoints, null caching for missing profiles, and variable TTL based on user activity level.
FAQ
Mini Project
Build a cache-aside layer for a product catalog API. Implement thundering herd prevention, batch loading for /api/products/ids=[...], null caching for 404s with short TTL, and cache invalidation on PUT /api/products/:id. Write a load test to measure the stampede prevention.
What's Next
Continue with Read-Through Caching to learn how read-through differs from cache-aside.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro