Skip to content

Read-Through Caching: Automatic Cache Population on Cache Miss

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Read. We cover key concepts, practical examples, and best practices to help you master this topic.

Read-through Caching moves the responsibility of populating the cache from the application to the cache layer itself. When a cache miss occurs, the cache automatically fetches the data from the database (or calls a loader function), caches it, and returns it — all transparently to the application.

sequenceDiagram
    participant App as Application
    participant Cache as Cache (Read-Through)
    participant DB as Database

    App->>Cache: GET key
    Cache->>Cache: Cache Miss
    Cache->>DB: SELECT * FROM table WHERE ...
    DB-->>Cache: Row Data
    Cache->>Cache: Store in Cache with TTL
    Cache-->>App: Return Data

    App->>Cache: GET same key
    Cache-->>App: Return Cached Data (Hit)

What You'll Learn

  • Implementing read-through caching with Redis client-side caching and custom loaders
  • Read-through vs cache-aside: when to use each
  • Cache loader functions and error handling
  • Distributed read-through with Redis Cluster

Why It Matters

Read-through caches simplify application code by removing explicit cache-check-populate logic. They also ensure consistent cache behavior across all services that use the same cache layer, preventing some services from forgetting to cache.

Real-World Use

A microservice architecture uses a shared Redis read-through cache for customer data. Each service calls cache.get('customer:123', loadCustomerFromDB). The cache layer handles miss population consistently, eliminating duplicate caching logic across 15 Microservices.

Read-Through Implementations

Custom Read-Through Cache

class ReadThroughCache {
  constructor(cacheBackend, loaderFn, ttl = 3600) {
    this.cache = cacheBackend;
    this.loader = loaderFn;
    this.ttl = ttl;
    this.pending = new Map();
  }

  async get(key) {
    const cached = await this.cache.get(key);
    if (cached !== null && cached !== undefined) {
      return JSON.parse(cached);
    }

    // Deduplicate concurrent misses for same key
    if (this.pending.has(key)) {
      return this.pending.get(key);
    }

    const promise = this.loadFromSource(key);
    this.pending.set(key, promise);
    try {
      return await promise;
    } finally {
      this.pending.delete(key);
    }
  }

  async loadFromSource(key) {
    try {
      const data = await this.loader(key);
      if (data !== null && data !== undefined) {
        await this.cache.setEx(key, this.ttl, JSON.stringify(data));
      }
      return data;
    } catch (err) {
      console.error(`Read-through load failed for ${key}:`, err.message);
      throw err;
    }
  }
}

// Usage
const userCache = new ReadThroughCache(
  redisClient,
  async (key) => {
    const id = key.replace('user:', '');
    const [rows] = await db.query('SELECT * FROM users WHERE id = ?', [id]);
    return rows[0] || null;
  },
  1800
);

Expected output:

First call: cache miss, loader fetches from DB, caches, returns. Subsequent calls: cache hit. Concurrent misses coalesce into one loader call.

Read-Through with Redis Client-Side Caching

const Redis = require('ioredis');

const redis = new Redis({
  host: 'redis-server',
  enableAutoPipelining: true,
  scripts: {
    getOrLoad: `
      local cached = redis.call('GET', KEYS[1])
      if cached then return cached end
      local data = redis.call('HGET', 'loaders', KEYS[1])
      return data
    `
  }
});

async function readThroughWithLua(key, loader) {
  const cached = await redis.call('GET', key);
  if (cached) return JSON.parse(cached);

  const data = await loader(key);
  if (data) {
    await redis.setex(key, 3600, JSON.stringify(data));
  }
  return data;
}

Expected output:

Lua script atomically checks cache and returns cached value. Application loader is called only on miss.

Read-Through with Node-cache-manager

const cacheManager = require('cache-manager');
const redisStore = require('cache-manager-ioredis');

const cache = cacheManager.caching({
  store: redisStore,
  redis: { host: 'redis', port: 6379 },
  ttl: 600
});

async function getSettings(userId) {
  return cache.wrap(`settings:${userId}`, async () => {
    const [rows] = await db.query('SELECT * FROM settings WHERE user_id = ?', [userId]);
    return rows[0] || {};
  });
}

Expected output:

cache.wrap() implements read-through: if key exists in cache, returns it. Otherwise calls the factory function, stores result, and returns it.

Common Mistakes

  • Using read-through for write-heavy data — the cache loader will be called on every read miss, but if data changes frequently, TTLs must be short, reducing efficiency.
  • Not handling loader failures — if the database is down, the read-through cache should not crash. Return stale cached data if available, or throw a graceful error.
  • Forgetting to invalidate the cache on writes — read-through does not handle this automatically. Combine with write-through or explicit invalidation.
  • Using a single loader function for all key types — different keys may need different loaders (e.g., user vs. product vs. order).
  • Not setting a TTL — without TTL, cached data lives forever and stale data is never refreshed.

Practice Questions

  1. How does read-through caching differ from cache-aside?
  2. What responsibility does the application lose when using read-through?
  3. How do read-through caches handle concurrent misses?
  4. What happens when the loader function throws an error?
  5. How do you invalidate specific keys in a read-through cache?

Challenge

Implement a read-through cache for a blog platform. Create a generic ReadThroughCache class that accepts different loader functions per key pattern (user:, post:, comment:*). Use Redis Sorted Sets to support paginated listing keys. Handle cache invalidation on writes.

FAQ

What is read-through caching?

Read-through caching is a pattern where the cache layer automatically fetches data from the database on a cache miss, transparently to the application. The application just calls get(key).

Is read-through better than cache-aside?

Neither is universally better. Read-through simplifies application code and ensures consistent caching. Cache-aside gives the application more control and is easier to debug.

How does read-through handle null values from the database?

The loader should return a sentinel value (or null) which the read-through cache stores with a short TTL to avoid repeated misses for nonexistent keys.

Can read-through work with Redis Cluster?

Yes, but the loader function runs in the application, not inside Redis. Redis Cluster handles key distribution; the application handles the read-through logic.

How do I combine read-through with write-through?

Use read-through for reads and write-through for writes. When writing, update the database and the cache atomically. Subsequent reads hit the updated cache.

Mini Project

Build a read-through cache library that wraps Redis. Support key-prefix-based loader registration (e.g., cache.registerLoader('user:*', loadUser)). Implement concurrent miss coalescing, error handling with stale data fallback, and automatic invalidation on writes.

What's Next

Continue with Cache Stampede to understand how to prevent thundering herd problems at scale.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro