intermediate~3h

System Design — Caching Patterns & CQRS

Caching and CQRS are the two system-design tools you reach for once a single database, however well-indexed, can't serve your read pattern fast enough or shaped correctly enough. Both show up constantly in system-design interviews, and both are easy to reach for too early.

Learning objectives

  • Name the five caching patterns and correctly choose one for a given read/write shape.
  • Diagnose cache stampede, penetration, avalanche, and hot-key failures from their symptoms, and state the standard fix for each.
  • Explain CQRS and identify a real scenario where it earns its added complexity.
  • Avoid reaching for caching or CQRS before a simpler fix (an index, a bigger cache-less database) has been tried.

Caching patterns and CQRS are where NoSQL's specific strengths (Redis's speed, MongoDB's flexible writes) actually get composed into real system designs — this is the "putting it all together" chapter that the rest of the NoSQL syllabus has been building toward.

PatternHow it worksRead path stays simple?Risk
Cache-aside (lazy loading)App checks cache; on miss, reads DB and populates cache itselfYes — app owns the logic, easy to reason aboutA cold cache (deploy, restart) sends every request straight to the DB at once
Read-throughA caching layer sits in front of the DB and handles the miss itself, transparentlyYes — app just talks to the cacheSame cold-start risk, hidden one layer deeper
Write-throughEvery write goes to cache and DB together, synchronouslyYes, and cache is never staleEvery write pays the latency of two systems, not one
Write-back (write-behind)Write goes to cache only; DB is updated asynchronously laterYes — writes are very fastA cache node dying before the async flush completes loses data that was never really "written"
Refresh-aheadCache proactively refreshes hot keys before they expire, based on access patternYesWasted refreshes for keys that turn out not to be accessed again

The overwhelmingly common default in real systems is cache-aside — it's the easiest to reason about, fails predictably (a cold cache is slow, not wrong), and each database's own book in this track (MongoDB, Redis, Cassandra) shows it wired up concretely with that specific database sitting behind the cache.

A single database optimized for both fast reads and fast writes at very high, very different scales is a genuinely hard engineering problem — caching patterns and CQRS are two established ways of accepting that a single data store can't do everything well, and deliberately splitting read and write paths instead.

FailureWhat happensStandard fix
Cache stampedeA hot key expires; hundreds of concurrent requests all miss at once and all hammer the DB simultaneously to refill itHave the first request to miss acquire a short lock and refill the cache while the rest wait/retry, instead of all of them querying the DB independently
Cache penetrationRepeated queries for a key that doesn't exist in the DB either — every request is a guaranteed miss, forever, since there's nothing to cacheCache the "not found" result too (with a short TTL), or use a Bloom filter in front of the DB to reject obviously-nonexistent keys cheaply
Cache avalancheA large batch of keys were all cached with the same TTL, so they all expire at the same instant, producing a stampede-like spike across many keys at onceAdd random jitter to TTLs so expirations spread out over time instead of landing simultaneously
Hot keyOne specific key (a celebrity's profile, a viral product) receives disproportionate traffic, overwhelming the single cache node/shard that owns itReplicate that specific key across multiple cache nodes, or add a short-lived local (in-process) cache layer in front of the shared cache for just that key

▲ Common mistake

Treating these four as one generic "add more cache" problem. Each has a genuinely different root cause and a different fix — throwing more cache capacity at a stampede or a hot key doesn't help, since the problem isn't total capacity, it's concurrent requests converging on one key at one instant.

Cache-aside — application checks the cache first, falling back to the database and populating the cache on a miss. Write-through — every write goes to the cache and the database together, synchronously. CQRS (Command Query Responsibility Segregation) — separating the write model (commands) from the read model (queries) entirely, often backed by different data stores optimized for each.

◆ Story — an order service pulled in two directions

An order service's write side needs strict validation, referential integrity, and ACID guarantees (Transaction Mastery, Chapters 02–05) — you cannot let an order reference a product that doesn't exist. Its read side, meanwhile, powers an analytics dashboard that needs to slice millions of historical orders by region, product category, and time window in under a second — a shape a normalized, write-optimized schema is actively bad at serving.

CQRS (Command Query Responsibility Segregation) is the pattern of splitting these into two genuinely separate models: a write model (normalized, ACID, optimized for correctness) and a read model (denormalized, often in a different database entirely, optimized for the specific queries it needs to serve) — connected by an event stream that propagates every write into an update of the read model, typically asynchronously.

SideOptimized forTypical store
Write (Command)Correctness, validation, referential integrityA relational database — see SQL Mastery and Transaction Mastery
Read (Query)Fast, flexible queries shaped for a specific screen or reportA denormalized document store (MongoDB) or a cache (Redis), built specifically for the read pattern

▲ Common mistake

Adopting CQRS for a normal CRUD screen where the read pattern and write pattern aren't actually in tension — this doubles your infrastructure (two data stores, an event pipeline, eventual-consistency bugs between them) for zero real benefit. CQRS earns its complexity specifically when the read and write shapes have genuinely diverged, the way the order-service story above did — not as a default architecture.

📖 Story

Imagine a restaurant kitchen deciding when to prepare a popular dish in advance versus cooking it fresh per order. Cache-aside is cooking fresh only when a customer orders it, then keeping a portion ready for the next similar order — the application checks the cache first, and on a miss, fetches from the database and populates the cache for next time. Write-through is preparing an extra portion of the dish for the display case every single time it's cooked for an order, keeping the display case (cache) always current, at the cost of extra work on every single write. Write-behind is jotting down what needs to go in the display case and updating it a little later, in a batch, prioritizing kitchen throughput over the display case being perfectly up to the second.

The three caching strategies, precisely

StrategyWhen cache is populatedTrade-off
Cache-asideOn a read miss (lazily)Simple, but the first request for any given key is always slow
Write-throughSynchronously, on every writeCache always fresh, but every write pays extra latency
Write-behindAsynchronously, after the writeFast writes, but a real window where the cache (and possibly the durable store) can lag

CQRS — separating the bookstore from the library card catalog

CQRS (Command Query Responsibility Segregation) separates the model used for writes (commands — "place this order") from the model used for reads (queries — "show me this customer's order history"). It's like maintaining a bookstore's inventory system (built for adding and removing stock accurately) completely separately from a public card catalog optimized purely for browsing and searching — the two serve fundamentally different access patterns, and forcing one data model to serve both well often means compromising on both. In practice, this often means the write side updates a normalized, transactionally-safe store, while a separate, denormalized read model (sometimes in a different database entirely) is built specifically to serve the actual read patterns fast, updated asynchronously from the write side's changes.

Cache-aside, mechanically

On a read: check the cache; if present (a hit), return it; if absent (a miss), read from the database, store the result in the cache, then return it. On a write: update the database, and either invalidate (delete) the corresponding cache entry or update it directly — invalidating is simpler and safer against subtle staleness bugs, at the cost of the next read being a guaranteed cache miss.

Write-through and write-behind, mechanically

Write-through updates the cache and the database as one logical operation on every write, synchronously — the client waits for both to complete, guaranteeing the cache is never stale, at the cost of every write now paying two operations' worth of latency instead of one. Write-behind acknowledges the write as soon as the cache is updated, then asynchronously flushes the change to the durable store shortly after — writes are fast, but there's a real window where a crash could lose data that was only in the cache and hadn't yet been flushed, which is precisely why write-behind is used mainly for data where that small loss window is an acceptable trade.

CQRS's read model synchronization, mechanically

The write side typically emits events ("OrderPlaced," "OrderShipped") when a command succeeds. A separate process consumes these events and updates the read model accordingly — often denormalizing the data into whatever shape makes the actual read queries fast (a single pre-joined, pre-aggregated table, for instance, instead of the write side's normalized tables). This event-driven update is usually asynchronous, which means the read model exhibits the same eventual-consistency lag introduced in the NoSQL Fundamentals chapter — a command that just succeeded may not be reflected in the read model for a brief moment afterward.

  • A product page cache in an e-commerce app — a natural cache-aside fit: most products are read far more often than they're updated, and a cache miss simply means one slightly slower request while the cache warms back up.
  • A user's account balance display that must never show stale data — a case for write-through (or no caching at all for that specific value), since the cost of an occasionally-stale balance is judged higher than the extra write latency write-through adds.
  • A high-throughput analytics event ingestion pipeline — a natural write-behind fit, batching many small writes into the durable store periodically, accepting a small window of potential data loss on a crash in exchange for dramatically higher write throughput.
  • An order management system using CQRS — the command side enforces strict business rules and transactional integrity when placing an order; a separate, denormalized read model powers a fast "order history" dashboard that would otherwise require expensive joins across the normalized write-side tables on every page load.
  • A social media feed — often built with CQRS-like separation: posting a new update goes through a strongly-consistent write path, while the actual personalized feed shown to each follower is pre-computed and stored in a separate, read-optimized model, updated asynchronously as new posts arrive.
  • Default to cache-aside for most read-heavy, write-light data — it's the simplest strategy to reason about and covers the majority of real caching needs well.
  • Reserve write-through for data where staleness is genuinely unacceptable and the extra write latency is an acceptable cost; reserve write-behind for high-throughput writes where a small, well-understood loss window is acceptable.
  • Prefer cache invalidation (deleting the stale entry) over cache updating on writes, where practical — it's simpler to reason about and less prone to subtle bugs where the cache and database silently diverge.
  • Reach for CQRS specifically when read and write access patterns are genuinely different enough that one shared model compromises both — not as a default architecture for every service, since it adds real synchronization complexity.
  • When adopting CQRS, be explicit about the read model's staleness window, and make sure the application (and its users) can tolerate a command's effects not being immediately visible in the corresponding query.

⚠️ Why this keeps happening

Caching and CQRS both look like straightforward wins at first glance — faster reads, cleaner separation of concerns — which makes it easy to underestimate the real synchronization complexity each one quietly introduces, until a stale cache or an unexpectedly-lagging read model causes a visible, confusing bug.

  • Forgetting to invalidate (or update) the cache on every code path that writes to the underlying data — one overlooked write path leaves a stale cached value that can persist far longer than anyone expects, since nothing else will naturally refresh it.
  • Using write-behind for data that actually can't tolerate any loss window, discovering the risk only after a crash loses writes that were still sitting in the cache, unflushed.
  • Adopting CQRS for a simple CRUD service with no real read/write asymmetry, adding real synchronization complexity (event handling, eventual consistency in the read model) for a problem a single, simple model would have solved just as well.
  • Not communicating the read model's staleness window to whoever consumes it — a user (or another engineer) who expects a command's effect to be immediately visible in a query can be genuinely confused, or file a bug report, over what's actually correct, if brief, eventual-consistency behavior.
  • Treating cache invalidation as an implementation detail rather than a first-class design concern — it's famously one of the harder problems in computer science precisely because every single write path has to remember to participate correctly, with no automatic enforcement backing it up.
  • Cache-aside's first-request-per-key latency (the "cold cache" cost) can be mitigated by proactively warming frequently-accessed keys ahead of expected demand, rather than always waiting for the first real request to populate them.
  • Write-through's added write latency can be reduced by writing to cache and database concurrently rather than sequentially, where the two operations don't depend on each other's result.
  • Write-behind's batching window is a direct lever — a larger batch improves throughput further but widens the potential data-loss window on a crash; tune it based on actual tolerance for loss, not just for raw throughput.
  • CQRS's read model, once separated, can be optimized purely for its actual query patterns (denormalized, pre-joined, even stored in a different, read-optimized database entirely) without being constrained by what the write side's transactionally-safe schema needs to look like.
  • Monitor cache hit rate as a first-class metric — a low hit rate suggests either poor key design, insufficient cache size, or access patterns that don't actually benefit from caching as much as assumed.

Cache invalidation bugs can serve stale, previously-valid authorization data (a user's old permission level) well past the point it should have expired — treat cache invalidation for any security-relevant field as a hard requirement, not a performance nice-to-have.

Track cache hit rate and staleness (time since last invalidation) as core metrics for any cache-aside/write-through system — a falling hit rate signals either a cache-sizing problem or a change in access patterns, and both are actionable long before they show up as user-facing latency.

  • Monitor cache hit rate, eviction rate, and staleness incidents as ongoing operational metrics, not just at initial rollout.
  • Add automated tests specifically covering every write path's cache invalidation, since a single missed path is exactly the kind of bug that's easy to introduce later and hard to notice without a dedicated test.
  • For write-behind caching, monitor the size of the pending, not-yet-flushed batch, and alert if it grows unexpectedly — a growing backlog signals the durable store isn't keeping up, and widens the crash-loss window in real time.
  • For CQRS systems, monitor the read model's lag behind the write side explicitly, and surface that staleness window transparently wherever it could matter to a user or another engineering team.
  • Revisit whether CQRS is still earning its complexity as a system's actual read and write patterns evolve — a service that no longer has meaningfully different read/write shapes may be better served by consolidating back to a single, simpler model.
  1. Implement cache-aside for a simple "get user by ID" endpoint, deliberately forget to invalidate the cache on an update path, and observe the stale-read bug this produces; then fix it.
  2. Implement write-through for the same data, and measure the added write latency compared to writing to the database alone.
  3. Design (in writing, or actual code) a small CQRS split for an order-processing system: a command side that validates and persists an order, and a separate read model updated via an event, optimized for a "customer order history" query.
  4. Simulate a crash in a write-behind caching setup by killing the process before its pending batch flushes, and observe which recent writes were lost — then discuss what data this strategy would and wouldn't be appropriate for.

✓ Quick recap

  • Cache-aside populates the cache lazily on a read miss; write-through updates the cache synchronously on every write; write-behind updates the cache immediately but flushes to the durable store asynchronously, trading a small loss window for write throughput.
  • Cache invalidation is genuinely hard because every single write path has to remember to participate — a missed path is a common, quiet source of stale-read bugs.
  • CQRS separates the write model (commands) from the read model (queries), letting each be optimized independently — but introduces a real, asynchronous staleness window between the two that must be communicated and tolerated.
  • Reach for CQRS specifically when read and write access patterns are genuinely different enough that a shared model compromises both, not as a default architecture.
  • Monitor cache hit rate and CQRS read-model lag as ongoing operational metrics — both patterns' real costs are easy to lose track of once initial rollout is done.

Want a visual for this concept?

Generate a diagram tailored to “System Design — Caching Patterns & CQRS” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to NoSQL Production Best Practices← Back to all NoSQL chapters