Hermes Wiki

Cache Population Strategies

Concept

Deciding what to cache is the easy half of a caching design; deciding how the cache gets filled and kept in sync with reads and writes is the half that actually determines correctness and latency behavior under load. Population strategy is a separate axis from eviction (what gets kicked out when the cache is full, see Cache Eviction Policies) and from invalidation (when a cached value stops being trusted, see Cache Invalidation Strategies) — this is specifically about the code path a read or a write takes through the cache layer.

Five named patterns cover almost every real system:

  • Cache-aside (lazy loading) — the application owns the logic. On a read, check the cache; on a miss, read the database directly and write the result into the cache before returning it. On a write, the application updates the database and either invalidates or updates the cache entry itself. The cache is a passive store the application drives.
  • Read-through — functionally similar to cache-aside on reads, but the miss-handling logic lives inside the cache layer itself (or a library wrapping it) rather than the application: the app just asks the cache for a key, and the cache transparently loads from the source of truth on a miss. The application code is simpler; the cache library needs to know how to reach the source of truth.
  • Write-through — every write goes to the cache and the database together, as one logical operation, before the write is considered complete. The cache is never allowed to hold a value that's behind what was just written.
  • Write-behind (write-back) — a write updates the cache immediately and returns, and the database update is flushed asynchronously afterward (batched, on a timer, or via a queue). This trades a real durability window (a crash before the flush loses the write) for much lower write latency.
  • Write-around — writes go straight to the database and skip the cache entirely; the cache only gets populated later, on a subsequent read (via cache-aside or read-through). This avoids filling the cache with data that was just written but may never be read again soon.

Tradeoffs

Strategy Write latency Staleness/durability risk Best fit
Cache-aside Not affected by cache (writes go straight to DB, cache updated/invalidated separately) Cache can briefly serve stale data between a write and its invalidation reaching the cache Read-heavy, general-purpose default; simplest mental model
Read-through Same as cache-aside on the read side; write behavior depends on pairing with write-through/around Same staleness window as cache-aside, but miss-handling logic is centralized in the cache layer Predictable, high-volume read patterns behind a caching library
Write-through Higher — every write pays the cost of a cache write plus a DB write, synchronously Lowest — cache is never behind the database for data written through it Data where the next read must reflect the write immediately (a user's own settings, a feature flag)
Write-behind Lowest — write returns after the cache update, before the DB write happens Highest — an unflushed write is lost if the cache node crashes before the async flush completes Write-heavy workloads where latency matters more than a small, accepted risk of losing the most recent writes
Write-around Not affected by cache (same as cache-aside) No staleness risk from the write path itself, but the first read after a write is always a cache miss Write-once, rarely-immediately-reread data (logs, audit events, a booking's initial creation) that would otherwise pollute the cache

The strategies aren't mutually exclusive across an application — a single system commonly runs cache-aside as the general read pattern, write-through on the specific fields where a user expects to see their own write reflected instantly, and write-around for high-volume write-once data that would otherwise thrash the cache with entries nobody re-reads.

When to use / when not to

  • Default to cache-aside: it requires no special cache-layer support, keeps the application in full control of what gets cached and when, and degrades gracefully (a cache outage just means every read falls through to the database, slower but correct).
  • Choose read-through over cache-aside mainly for code cleanliness when using a caching library or service that already implements the load-on-miss logic — the functional behavior is nearly identical, so this is largely a "who owns the miss-handling code" decision rather than a correctness one.
  • Reach for write-through specifically on paths where staleness is unacceptable even for a moment — anything a user or another system would notice as wrong if they read it back immediately after writing.
  • Reach for write-behind only after accepting its crash-loss window explicitly — this needs either data where losing the last few writes on a crash is tolerable, or a durable buffer (a write-ahead log, a persistent queue) in front of the async flush to bound that risk.
  • Use write-around for data that's written once and has a low chance of being read again in the near term — populating the cache on a write nobody's about to re-read just evicts data that would have been a genuine cache hit.
  • Don't use write-through or write-behind by default "to be safe" — both add real machinery (synchronous dual-writes, or an async flush pipeline with its own failure modes) that cache-aside doesn't need, and neither is free.

Common pitfall

Picking a population strategy per system instead of per data type, and ending up with a single global policy that's wrong for at least some of what's cached. A system that writes everything through the cache synchronously (write-through everywhere) pays that write-latency tax even on data nobody needs read-your-own-write guarantees for; a system that lazy-loads everything (cache-aside everywhere) creates avoidable staleness windows on the specific fields — a feature flag toggle, a user's own profile edit — where the next read really does need to reflect the write immediately. The fix isn't a smarter single strategy, it's recognizing that population strategy is a per-access-pattern decision, and that most non-trivial systems legitimately run more than one at once.

Engineering Lens

The population-strategy decision is where a lot of "the cache showed stale data" incidents actually originate, and it's rarely because the wrong strategy was picked in the abstract — it's because a data type's access pattern changed (a field that used to be write-once became frequently re-read soon after write, or a field that needed instant read-your-writes started being safe to lazy-load) and nobody revisited the population strategy attached to it. The strong answer in review isn't naming which of the five patterns is in use — it's being able to say, for any specific cached field, what read-after-write guarantee the product actually needs, and showing the population strategy chosen delivers exactly that guarantee and no more machinery than that.

Sources

Hermes Wiki