A distributed cache can reduce repeated database reads, external calls, or expensive computation across many application instances. It can also make an already complicated system harder to reason about while temporarily masking an inefficient query, poor data model, or uncontrolled workload.

The first caching question is not “Which cache should we use?” It is “Which measured cost are we trying to remove?”

Locate the bottleneck before adding a cache

Trace the slow or capacity-limited journey. Separate time spent waiting for database connections, executing queries, acquiring locks, reading storage, calling dependencies, serializing data, and performing application computation. Inspect both latency and resource saturation.

Repeated reads of the same relatively stable value may be a strong cache candidate. An expensive deterministic computation with shared inputs may also benefit. A slow query caused by a missing index or a request pattern that loads unnecessary rows may be better repaired at the source. Mastering Database Indexes covers that decision in more detail.

A high cache hit ratio is not proof of value. It can include cheap requests that never needed caching. Measure the protected resource and the user journey: did source load, tail latency, or contention improve without unacceptable staleness or new failure modes?

Caching is not a requirement for horizontal scaling. Stateless application instances can share a database or service without sharing a cache. Introduce the cache because the workload and freshness contract justify it, not because a generic architecture diagram includes one.

Define the freshness contract

Every cached value has an owner and an acceptable age. “Eventually consistent” is too broad to guide implementation. State whether the value may be seconds or minutes old, whether users can observe their own recent writes, and which operations require authoritative data.

Consider a report summary whose source computation is expensive. If the product accepts a five-minute-old result, a five-minute TTL may be reasonable for that specific summary. That is an explicit product assumption, not a universal caching recommendation. A balance used to authorize a transaction may have a very different contract and may not be safe to serve from the same caching pattern.

Include key dimensions such as tenant, permissions, locale, filters, and schema version. Omitting an authorization or tenant dimension can become a data-isolation defect, not merely a stale result.

Pick an update pattern deliberately

With cache-aside, the application checks the cache. On a miss, it loads the source of truth, returns the result, and populates the cache. This keeps the cache optional to the write path, but the first request after expiry pays the source cost and concurrent misses can duplicate work.

With read-through, a cache layer performs the load on a miss. This centralizes loading behavior but does not remove freshness or failure decisions; they move into the cache integration.

With write-through, the write path updates the cache and source under the pattern’s defined ordering. It can make fresh values available quickly, but partial failure and consistency between the two stores must be handled.

With write-behind, changes reach the source asynchronously. This can absorb bursts, but the cache becomes part of the durable-write story and introduces loss, ordering, replay, and recovery concerns. Do not use it merely to make a slow write appear fast.

Many production systems combine cache-aside reads with invalidation after a committed source update. The simple diagram is not the full protocol: ordering and concurrent readers still matter.

Invalidation is a data-ownership problem

An expiry bounds how long a value may remain cached after its creation. It does not guarantee immediate freshness after an update. If the owning module knows which mutation changes a value, it can delete or replace related keys after the source transaction commits.

Deletion is often safer than trying to reconstruct every cached representation during the write. The next reader rebuilds from the source. However, a race is possible: a reader can load an old source value, a writer can commit and invalidate, and then the reader can populate the old value after invalidation.

Mitigations include versioned keys, compare-and-set behavior, generation numbers, event ordering, or accepting the race within a documented freshness bound. Choose based on impact; complexity should match the data contract.

Key versioning is useful during schema or interpretation changes. A version prefix allows new code to stop reading old-format entries without a dangerous full-cache delete. Old keys then expire or are removed through a controlled cleanup.

Prevent stampedes and hot keys

When a popular key expires, many requests can miss together and hit the source simultaneously. This cache stampede can create a source outage precisely when the cache is providing the least protection.

Options include:

  • request coalescing so one loader refreshes a key while others wait;
  • a bounded distributed lock with careful timeout and ownership behavior;
  • jittered TTLs to avoid many keys expiring at once;
  • refresh-ahead for predictable, frequently used values;
  • serving a known stale value briefly while one request refreshes it.

Each option makes assumptions. A lock service can fail. Serving stale data needs a maximum stale age. Refresh-ahead can waste work on values no longer requested. Make those assumptions visible.

A hot key can also overload one cache shard or network path even with a high hit ratio. Replication, local near-caches, key splitting, or a changed access pattern can help, but each affects invalidation and consistency.

Decide what happens when the cache fails

“The cache is optional” is true only if the source can safely absorb misses. During a cache outage, every request may bypass to a database sized for the normally reduced load. That fallback can turn one outage into two.

Choose among protected bypass, stale serving, reduced functionality, or explicit failure. Protected bypass uses rate limits, concurrency limits, or circuit breakers to preserve source capacity. Stale serving requires retaining an eligible old value and marking the maximum age. Reduced functionality may omit an expensive summary while preserving the transaction path.

Recovery needs control too. An empty cache can cause a cold-start surge. Warm a bounded set of genuinely hot entries, ramp traffic, or limit concurrent source loads. Do not attempt to repopulate every possible key without evidence that it will be used.

Eviction under memory pressure is another form of miss generation. Monitor memory, eviction rate, hit and miss cost, source load, key-size distribution, and latency. The companion article Redis in Production examines memory growth, large keys, TTLs, and eviction in the Redis layer itself.

Treat the cache as part of reliability

A shared cache adds network calls, fleet capacity, deployment behavior, security controls, and operational ownership. Test cache unavailability, latency, partial shard loss, mass expiry, and source protection. Verify that dashboards distinguish a healthy cache from a healthy user journey.

The ERP performance and reliability case study demonstrates the diagnostic principle: follow the real workflow and validate the bottleneck before selecting the intervention. A performance and reliability assessment applies the same approach when latency, database pressure, and failure recovery cross several components.

Distributed caching is valuable when repeated work is costly and bounded staleness is acceptable. It becomes dangerous when it substitutes for diagnosis or when invalidation and outage behavior remain implicit. Find the bottleneck, define freshness, protect the source, and make recovery part of the design.