Read-Through Cache Low-Level Design: Cache Population, Stale-While-Revalidate, and Consistency Patterns

What Is a Read-Through Cache?

In a read-through cache, the cache layer itself is responsible for loading data from the database on a miss. The application calls cache.get(key) and always receives a value — the cache either returns a cached entry or transparently fetches it from the DB, populates the cache, and returns it. The application has no explicit cache-miss handling logic.

This is in contrast to a cache-aside (lazy loading) pattern, where the application checks the cache, handles a miss itself by querying the DB, and manually populates the cache. Read-through centralizes that logic in the cache layer.

Cache Loader: Pluggable Population

A cache loader is a function registered per key pattern that the cache calls on a miss. For example:

  • Key pattern user:* → loader queries the users table by user ID.
  • Key pattern product:* → loader queries the products table.

Loaders are registered at startup. The cache calls loader(key) when the pattern matches a missed key, populates the cache with the result, and returns the value to the caller — all transparently.

Stale-While-Revalidate

Stale-while-revalidate (SWR) is a technique to eliminate cache-miss latency at the cost of brief staleness. Instead of blocking the caller while the cache refreshes an expired entry:

  1. Serve the stale value immediately.
  2. Trigger an async background refresh to reload from DB.
  3. The next request (after refresh completes) gets the fresh value.

SWR requires two TTL values per entry: a fresh TTL (serve without refresh) and a stale TTL (serve stale while refreshing). Once the stale TTL expires, the entry is considered fully expired and the next request blocks on a synchronous load.

Versioned Cache Entries

For stronger consistency, entries carry a version_id sourced from the DB row (e.g., a row-level version counter or updated_at timestamp). On a write to the DB:

  1. The DB increments the version on the row.
  2. The write path sends an invalidation message (or updates the cache directly) with the new version.
  3. On next read, the cache compares its stored version against the expected version. A version mismatch triggers a synchronous reload.

This gives fine-grained invalidation without a blanket TTL-based expiry.

Consistent Hashing Across Cache Nodes

In a distributed read-through cache cluster, consistent hashing ensures that a given key always maps to the same cache node. This is critical for read-through correctness: if different nodes could serve the same key, they would each maintain separate loader state and potentially issue redundant DB queries. With consistent hashing, a cache miss is always handled by the same authoritative node for that key.

Background Pre-Fetch

A pre-fetch job scans the cache for entries approaching their TTL expiry and proactively refreshes them before they expire. This eliminates the latency spike that would occur if many entries expired simultaneously (thundering herd). Pre-fetch candidates are identified by querying access frequency — only frequently accessed entries are worth the pre-fetch cost.

SQL Schema

CREATE TABLE CacheEntry (
    cache_key    TEXT PRIMARY KEY,
    value        JSONB        NOT NULL,
    version_id   BIGINT,
    loaded_at    TIMESTAMPTZ  NOT NULL DEFAULT now(),
    expires_at   TIMESTAMPTZ  NOT NULL
);

CREATE INDEX idx_cache_expiry ON CacheEntry (expires_at);

CREATE TABLE CacheLoader (
    key_pattern    TEXT PRIMARY KEY,
    loader_type    TEXT         NOT NULL,
    ttl_seconds    INT          NOT NULL,
    stale_ttl_seconds INT       NOT NULL
);

Python Implementation Sketch

import time, threading
from typing import Callable, Optional

class ReadThroughCache:
    def __init__(self, db):
        self.db = db
        self.store: dict[str, dict] = {}
        self.loaders: dict[str, Callable] = {}
        self.lock = threading.Lock()

    def register_loader(self, key_pattern: str, loader_fn: Callable, ttl: int, stale_ttl: int):
        self.loaders[key_pattern] = {'fn': loader_fn, 'ttl': ttl, 'stale_ttl': stale_ttl}

    def get(self, key: str) -> Optional[dict]:
        with self.lock:
            entry = self.store.get(key)
        now = time.time()
        if entry:
            if now < entry['expires_at']:
                return entry['value']
            elif now  Optional[dict]:
        loader_cfg = self._find_loader(key)
        if not loader_cfg:
            return None
        value = loader_cfg['fn'](key)
        if value is None:
            return None
        now = time.time()
        entry = {
            'value': value,
            'expires_at': now + loader_cfg['ttl'],
            'stale_expires_at': now + loader_cfg['ttl'] + loader_cfg['stale_ttl'],
        }
        with self.lock:
            self.store[key] = entry
        return value

    def stale_while_revalidate(self, key: str):
        self.load_from_db(key)

    def invalidate_version(self, key: str, new_version: int):
        with self.lock:
            entry = self.store.get(key)
            if entry and entry.get('version_id', -1) < new_version:
                del self.store[key]

    def _find_loader(self, key: str):
        import fnmatch
        for pattern, cfg in self.loaders.items():
            if fnmatch.fnmatch(key, pattern):
                return cfg
        return None

Consistency with Write Operations

Read-through caches must be paired with a consistent write strategy. Options:

  • Write-through: update cache and DB synchronously on every write — cache always current.
  • Write-invalidate: on write, delete the cache entry; next read reloads from DB via the loader.
  • Version-based invalidation: as described above, propagate new version to trigger stale detection.

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Anthropic Interview Guide 2026: Process, Questions, and AI Safety

Scroll to Top