A cache stores the results of expensive or frequent reads in fast memory, usually a key-value store such as Redis or Memcached, so the database does less work and responses are quicker. Caching is effective, but it introduces a second copy of the data, and keeping that copy correct is the main challenge.

When caching helps

Caching works best for data that is read much more often than it is written and that can be slightly stale without harm: product details, configuration, rendered fragments, computed aggregates and session data. It helps less, or adds risk, for data that changes constantly or must always be exactly current, such as account balances used to authorize payments.

Cache-aside (lazy loading)

Cache-aside is the most common pattern. The application:

  1. looks for the key in the cache;
  2. on a hit, returns the cached value;
  3. on a miss, reads from the database, stores the result in the cache with an expiry and returns it.

Only data that is actually requested is cached, and a cache failure can fall back to the database. The drawback is that the first request after a miss is slow, and the cached value may be stale until it expires or is invalidated.

Read-through

In read-through caching, the cache layer itself loads missing data from the database, so the application only talks to the cache. The behaviour is similar to cache-aside, but loading logic lives in a library or caching service.

Write-through

With write-through, every write goes to the cache and the database together, so the cache always holds current values for written keys. Writes are slower, and the cache may fill with data that is never read. Write-through is often combined with cache-aside for reads.

Write-behind

Write-behind (or write-back) writes to the cache first and updates the database asynchronously. It can absorb bursts of writes, but if the cache fails before data is persisted, writes can be lost. It needs careful design and is less common for primary business data.

Invalidation strategies

Stale data is the price of caching. Common ways to limit it:

  • Time to live (TTL): every entry expires after a set time. Simple and robust; choose the TTL based on how stale the data may be.
  • Delete on write: when the application updates a record, it deletes the cached key so the next read reloads it. Deleting is usually safer than updating the cached value, which can race with concurrent writes.
  • Versioned keys: include a version or timestamp in the key, so new data automatically uses a new key.
  • Event-driven invalidation: change events from the database, for example via change data capture, trigger cache deletes.

A TTL is a useful safety net even when you invalidate explicitly, because it limits the damage of a missed invalidation.

Avoiding cache stampedes

When a popular key expires, many requests can miss at once and all hit the database together. Mitigations include adding random jitter to TTLs, letting a single request recompute the value while others wait or receive the old value, and refreshing hot keys shortly before they expire.

Other practical concerns

  • Design keys clearly, for example product:123:v2, and keep them consistent.
  • Cache negative results (such as "not found") briefly to avoid repeated database lookups for missing data.
  • Set memory limits and an eviction policy, and monitor hit rate.
  • Treat the cache as disposable: the application must work, more slowly, if it is empty.

Key takeaways

  • Cache-aside with a TTL is a sound default for read-heavy data.
  • Delete cached entries on write rather than updating them in place.
  • Protect against stampedes and keep the database as the source of truth.
About Integratedatabase

Expert insights and resources in technology. Learn more.

Newsletter