Skip to content
System Design for Software Engineering Interviews

Caching

Last verified against its sources on 23 September 2026

A cache buys speed by keeping a copy of data closer to where it's read, and every gain it offers comes with a staleness or memory cost to manage on purpose. This module covers cache-aside and write-through as the two basic population strategies, TTLs and eviction policies as the two independent tools for bounding staleness and memory, and Redis's specific character as an in-memory store — including when a cache actually needs to survive a restart at all.

Caching Strategies

  • Apply the cache-aside (lazy loading) pattern to cut load on a backing store, and explain when it leaves stale data behind.
  • Contrast cache-aside with write-through, and choose between them based on a stated read/write ratio.

A cache sits between an application and the data store it's slow to query directly, and the simplest way to use one is cache-aside, sometimes called lazy loading. AWS's own documentation on ElastiCache caching strategies describes the flow plainly: the application asks the cache first; on a hit, the cache returns the data directly; on a miss, the application queries the backing store itself, then writes the result into the cache before returning it, so the next request for that key is a hit. Nothing is loaded into the cache until something actually asks for it, which keeps the cache small and relevant to real traffic — but it also means a cache that was just cleared (deployed, restarted, evicted under memory pressure) starts genuinely cold, and the first wave of requests after that pays the full backing-store cost.

On a hit, the app never needs to contact Store at all.

Cache-aside has a specific weakness AWS's documentation names directly: because the cache is only populated on a read miss, nothing updates it when the underlying data changes through a different path, so a value already sitting in the cache can quietly go stale until it expires or is evicted. Write-through closes that gap by writing to the cache at the same time as the backing store, so a cache entry is never allowed to fall behind — the cost is that every write now pays for two operations instead of one, and any key nobody has read yet still isn't in the cache until it's requested (or pre-warmed). The two aren't mutually exclusive: a design commonly uses cache-aside for the bulk of its reads and adds write-through, or a short TTL, specifically on the fields that can't tolerate staleness.

The choice tracks the read/write ratio estimated earlier. A read-heavy path — a product page, a user profile — gets most of its value from cache-aside alone: most requests are reads, most of those reads hit the cache, and the rare write can simply let the stale cached copy expire on its own TTL. A path where reads need to reflect a write almost immediately — an inventory count right before checkout — needs write-through, or an explicit invalidation on write, because waiting out a TTL risks selling something that's already gone. Naming which fields fall into which category, out loud, is the same move as naming a non-functional requirement: it turns "we added a cache" into a decision an interviewer can evaluate.

A cache-aside cache was just flushed after a deploy. What happens to the first wave of requests, and why?Answer it yourself first, then open this.

They all miss and hit the backing store directly, since cache-aside only populates an entry after something actually requests it.

Invalidation and Eviction

  • Use a TTL to bound how stale a cached value can get, and explain which operations preserve or clear it.
  • Choose an eviction policy that matches how a cache's keys are actually accessed once it's full.

A TTL (time-to-live) is the simplest invalidation strategy available: attach an expiry to a cache entry when it's written, and let it disappear on its own rather than trying to explicitly invalidate it from every place the underlying data could change. Redis's own documentation on EXPIRE is specific about which operations respect that boundary: the timeout is cleared only by commands that delete or overwrite a key's contents outright — DEL, SET, GETSET, the *STORE commands — while an operation that alters the value without replacing it, like INCR on a counter or HSET on one field of a hash, leaves an existing timeout untouched. That distinction matters in practice: a cache entry given a TTL and then incrementally updated with a field-level command keeps counting down to the same deadline — it doesn't reset.

A TTL bounds staleness, but it doesn't bound memory — a cache can still fill up before anything expires. That's what an eviction policy is for: Redis's documentation lists several, and picking the right one is a match to the access pattern, not a default you leave alone. allkeys-lru evicts whichever key hasn't been touched in the longest time, which fits a workload where a subset of keys stays popular and the rest fades; allkeys-random fits a workload where keys are read in a roughly even, cyclical pattern, where recency carries no signal at all; volatile-ttl evicts whichever key with an expiry is closest to expiring anyway, which the documentation suggests specifically when the application can already estimate which keys are good candidates for early eviction. The volatile-* policies only ever touch keys that were given a TTL in the first place — a key with no expiry is untouchable to them, no matter how memory-starved the cache gets.

A cache entry is created and given a 60-second expiry with EXPIRE. Ten seconds later, one field of it is updated with HSET. Does the key still expire at the original 60-second mark?Answer it yourself first, then open this.

Yes — HSET alters the hash without overwriting the whole key, so per Redis's own documentation it leaves an existing timeout untouched.

Redis as a Cache

  • Explain why Redis's in-memory design makes it fast as a cache, and what that costs on a restart.
  • Choose whether a Redis-backed cache needs persistence at all, based on what a cold cache would cost the system behind it.

Redis is an in-memory data store first — every read and write is served from RAM, which is what makes it fast enough to sit in front of a database on every request without becoming the new bottleneck. That speed comes with an obvious question: what happens to the data if the process restarts? Redis's own documentation on persistence lays out the honest answer: by default you have a choice, not a guarantee. RDB persistence takes point-in-time snapshots of the whole dataset at intervals; AOF persistence logs every write as it happens and replays the log on startup; the two can be combined; and — the option the documentation calls out explicitly as common for caching — persistence can be turned off entirely.

Whether to persist a Redis cache at all is a direct consequence of what a cold cache costs the system behind it, which is exactly what this module already worked out for cache-aside: a cache-aside cache that loses everything on restart just means every key misses once and gets repopulated from the backing store on the next read, at the cost of a brief spike in load on that store. If the backing store can absorb that spike, skipping persistence is the right call — it's one less thing to operate, and RDB's periodic fork-and-write has a real cost of its own. If the cache is standing in for data the backing store can't cheaply regenerate — a computed leaderboard, a session store with no other copy of "who's logged in" — losing it on restart is a real outage, not an inconvenience, and that's when RDB, AOF, or both earn their operational cost.

A team debates whether to enable AOF persistence on a Redis cache that sits in front of a Postgres database using cache-aside. What's the deciding question?Answer it yourself first, then open this.

Whether Postgres can absorb the load of every cached key missing at once after a restart — if it can, skipping persistence is fine; if not, persistence (or a warm-up step) is worth the cost.

Sources