Skip to content

Cache-Aside Pattern: The Most Common Caching Strategy Explained

DodaTech Updated 2026-06-28 4 min read

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

  1. In cache-aside, when is the cache populated?
  2. How does cache-aside handle data updates without write-through?
  3. What is the thundering herd problem in cache-aside?
  4. Why should you cache null values from the database?
  5. 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

What is cache-aside?

Cache-aside (lazy loading) is a pattern where the application checks the cache first. On a miss, it fetches data from the source, stores it in the cache, and returns it. On a hit, it returns cached data.

How does cache-aside handle data updates?

Cache-aside does not handle updates automatically. The application must explicitly invalidate or update the cache when data changes, or rely on TTL expiry.

What is the main disadvantage of cache-aside?

The first read after a cache miss or TTL expiry is slow (the read penalty). Also, data can be stale if updates are not invalidated in the cache.

Should I use cache-aside or read-through?

Cache-aside gives application code full control over caching. Read-through uses the cache library to fetch from the source automatically. Cache-aside is more flexible; read-through is simpler.

How do I prevent cache stampede in cache-aside?

Use request coalescing: when multiple requests miss the same key, only one fetches from the source. The others await the same promise. Also use jittered TTLs to prevent mass expiry.

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