Background
Sections
IntroductionRequirements & Problem AnalysisConstraints & AssumptionsEstimation TechniquesFunctional vs Non-Functional RequirementsMoSCoW PrioritizationSystem Design FundamentalsArchitecture DiagramClass DiagramComponent DiagramData Flow Diagram (DFD)ER Diagram (Entity-Relationship Diagram)High Level Design (HLD)Low Level Design (LLD)Sequence DiagramState DiagramUse Case DiagramData StorageDocument StoresFile StorageGraph DatabasesIn-Memory DatabasesKey-Value StoresNewSQLNoSQL DatabasesObject StorageSQL Databases (RDBMS)Time-Series DatabasesWide-Column StoresDatabase ConceptsACID PropertiesCAP TheoremConsistency ModelsIndexingNormalization & DenormalizationReplicationSharding & PartitioningTransactions & Isolation LevelsScalabilityAuto-Scaling & ElasticityConsensus & Leader ElectionLoad BalancingReplication & Read ReplicasSharding & PartitioningVertical vs Horizontal ScalingAvailability & ReliabilityBackup & Data DurabilityCircuit BreakerData ConsistencyDisaster RecoveryFault Tolerance & FailoverGraceful DegradationHigh AvailabilityNetworkingCDNDNSFirewalls & VPNHTTP & HTTPSLoad Balancer & Reverse ProxyTCP/IP & UDPWebSocketsCachingCache InvalidationCache Read/Write PatternsCaching LayersEviction PoliciesRedis vs MemcachedMessaging & CommunicationDead-Letter QueueIdempotencyKafka vs RabbitMQ vs SQSMessage QueuesPub/SubCompute & ServicesAPI GatewayContainers & OrchestrationMonolith vs MicroservicesServerlessService DiscoveryService MeshWeb Server & App ServerAPI DesignAPI Versioning & IdempotencyAuthentication & AuthorizationGraphQLgRPCPaginationRate Limiting & ThrottlingRESTSecurityAuthentication & AuthorizationData PrivacyEncryptionInput Validation & InjectionOAuth2 & JWTSecrets ManagementXSS & CSRFStorage & File SystemsBackup & RetentionBlock vs File vs Object StorageData Lakes & WarehousesDistributed File SystemsEphemeral StorageObservability & MonitoringDistributed TracingHealth ChecksLoggingMetricsSLI, SLO, SLADesign PatternsBulkhead PatternCircuit Breaker PatternCreational PatternsRate Limiter PatternRetry PatternStructural & Behavioral Patterns

Cache Read/Write Patterns

7 min read

In a Nutshell

Once you decide to cache, you must decide how the cache and the database stay coordinated — who reads from what, who writes to what, and in what order. These are the caching patterns, and each makes a different trade-off between read speed, write speed, consistency, and complexity. The read patterns (cache-aside and read-through) differ in whether your application or the cache manages loading on a miss. The write patterns (write-through, write-behind, write-around) differ in when and how writes reach the database. Picking the right combination is one of the most consequential — and most commonly botched — caching decisions.

2D minimalistic diagram showing the cache-aside pattern: an application in the center with arrows to a cache (top) and a database (bottom); on a read it checks the cache first (hit returns, miss goes to the database then populates the cache), illustrated with numbered steps 1-check cache, 2-miss, 3-read DB, 4-write cache, 5-return

How It Actually Works

Read Patterns

Cache-Aside (Lazy Loading) — the most common. The application orchestrates: check the cache; on a miss, read the database and populate the cache itself.

read(key):
  value = cache.get(key)
  if value is None:            # cache MISS
      value = db.query(key)
      cache.set(key, value, ttl)
  return value
  • ✅ Only requested data is cached (memory-efficient); cache failure doesn't break reads (fall back to DB).
  • ⚠️ First request per key is slow (miss); risk of stale data; app code must handle the logic.

Read-Through — the cache sits in front of the DB and loads missing data itself, transparently to the app.

read(key):
  return cache.get(key)        # cache loads from DB on miss internally
  • ✅ App code is simpler; loading logic centralized in the cache layer.
  • ⚠️ Requires cache support/config; first-read latency still exists.

Write Patterns

Pattern Write Path Consistency Write Latency Risk
Write-Through Write to cache and DB synchronously Strong (cache always fresh) Higher (two writes) Slower writes
Write-Behind (Write-Back) Write to cache now, DB asynchronously later Eventual Lowest Data loss if cache dies before flush
Write-Around Write directly to DB, skip cache Cache may be stale until next read Normal Cache miss on next read
Write-Through:   App → Cache → DB   (both updated before ack; always consistent)
Write-Behind:    App → Cache → [ack]         ... later ... Cache → DB (batched)
Write-Around:    App → DB           (cache untouched; populated lazily on read)

Choosing a Combination

Read and write patterns combine. Common pairings:

Combination Good For
Cache-aside + write-around Read-heavy data written infrequently; avoids caching write-only data
Read-through + write-through Read-heavy data needing freshness; simpler app code
Cache-aside + write-through Balanced; cache kept fresh on write, populated on read
Write-behind (with either) Write-heavy workloads that tolerate eventual persistence (metrics, counters)

The Classic Cache-Aside + Write Consistency Trap

The order of operations on a write matters enormously. A naive "update DB, then update cache" has a race condition:

❌ Update cache then DB (or DB then cache) both have races:
   Two concurrent writers can interleave and leave the cache holding
   an older value than the database.

✅ Safer: update the DB, then INVALIDATE (delete) the cache entry.
   The next read misses and repopulates from the fresh DB value.
   Deleting is safer than updating because a stale delete just causes
   a harmless re-fetch, not a persistent wrong value.

This is why the widely-recommended pattern is cache-aside for reads + write to DB then delete the cache key — invalidation, not update. (More in Cache Invalidation.)

Guarding Against the Thundering Herd

When a popular key expires, many concurrent requests all miss and stampede the database simultaneously:

Popular key "homepage" expires at t=0.
1000 requests arrive at t=0 → all miss → all hit the DB at once → 💥

Mitigations: a mutex/lock so only one request recomputes while others wait, early/probabilistic recomputation before expiry, or serving stale-while-revalidate.

2D minimalistic comparison of the three write patterns as three rows: "Write-Through" shows the app writing to both cache and database at the same time with a checkmark for consistency; "Write-Behind" shows the app writing to cache immediately and a delayed dashed arrow batching to the database later; "Write-Around" shows the app writing only to the database, bypassing the cache

Seeing It in Action

Scenario: Cache-aside with safe invalidation and stampede protection in a web app.

def get_product(product_id):
    key = f"product:{product_id}"
    value = cache.get(key)
    if value is not None:
        return value                        # HIT

    # MISS — use a lock so only ONE request recomputes (no stampede)
    with cache.lock(f"lock:{key}", timeout=5):
        value = cache.get(key)              # double-check after acquiring
        if value is None:
            value = db.query("SELECT * FROM products WHERE id=%s", product_id)
            cache.set(key, value, ttl=300)  # 5-min TTL
    return value

def update_product(product_id, data):
    db.update("UPDATE products SET ... WHERE id=%s", product_id, data)
    # Write DB first, then INVALIDATE (delete) — not update — the cache key.
    cache.delete(f"product:{product_id}")
    # Next read misses and repopulates from the now-fresh DB value.

Why this is the pragmatic default: cache-aside keeps the app in control and degrades gracefully if the cache is down (reads fall back to the DB). Writing to the DB then deleting the key avoids the update-ordering races that plague "update the cache" approaches. The per-key lock prevents a popular key's expiry from stampeding the database. This combination — cache-aside, delete-on-write, lock-on-miss — handles the vast majority of real-world caching correctly.

Interview Questions

  1. Q: Explain the cache-aside pattern and its main advantages and drawbacks. Hint: The application checks the cache; on a miss it reads the DB and populates the cache itself. Advantages: only requested data is cached (memory-efficient), and the cache being down doesn't break reads (fall back to DB). Drawbacks: first request per key is slow (cold miss), the app owns the logic, and stale data is possible — usually handled with TTLs and delete-on-write.

  2. Q: Compare write-through, write-behind, and write-around. Hint: Write-through writes cache + DB synchronously (always-fresh cache, slower writes). Write-behind writes to cache immediately and flushes to DB asynchronously (fastest writes, eventual persistence, risk of loss if cache dies). Write-around writes only to the DB, leaving the cache to populate lazily on read (avoids caching write-only data, but next read misses).

  3. Q: On a cache-aside write, why delete the cache key instead of updating it? Hint: Updating the cache introduces races — concurrent writers can interleave and leave the cache holding a stale value that persists. Deleting (invalidating) is safer: the next read simply misses and repopulates from the fresh DB value. A spurious delete only causes a harmless re-fetch, whereas a wrong update persists incorrect data. Order: write DB first, then delete the key.

  4. Q: What is a thundering herd / cache stampede and how do you prevent it? Hint: When a popular key expires, many concurrent requests all miss and hit the database simultaneously, overwhelming it. Prevent with a mutex/lock so only one request recomputes while others wait or serve stale, probabilistic early recomputation before expiry, or stale-while-revalidate (serve the old value while refreshing in the background).

  5. Q: Which read/write pattern combination would you choose for a read-heavy product catalog, and why? Hint: Cache-aside (reads) + write-around or delete-on-write. The catalog is read far more than written, so lazy-loading only caches what's actually requested, and writing to the DB then invalidating keeps things simple and consistent. Write-through is also reasonable if you want the cache always warm; write-behind is unnecessary since writes are infrequent.

References

Dive Deeper