System Design

Caching Strategies Explained

Learn common backend caching strategies, including cache-aside, write-through, write-behind, TTLs, invalidation, and cache stampede risks.

Diagram showing cache-aside, database fallback, TTL expiration, and refresh paths.

Quick answer

Caching stores data closer to where it is needed so future requests can be served faster or with less load on the primary system.

Backend caching improves latency and scalability, but it also introduces freshness, invalidation, consistency, and failure-mode tradeoffs. The right strategy depends on what data is cached, how stale it can be, and what happens when the cache is wrong or unavailable.

Why caching helps

Many backend requests repeat the same work:

  • Load the same user profile.
  • Read the same product catalog entry.
  • Render the same expensive report summary.
  • Fetch the same configuration.
  • Query the same reference data.

If each request hits the database or an external service, latency and load grow quickly. A cache can serve repeated reads from memory or an edge location instead.

Caching is most useful when reads are frequent, data changes less often than it is read, and slightly stale data is acceptable.

Cache-aside

Cache-aside is one of the most common backend patterns.

The application checks the cache first. If the item is missing, it reads from the database and writes the result into the cache.

request -> cache lookup
        -> cache hit: return cached value
        -> cache miss: read database, store cache, return value

Pseudo-code:

UserProfile profile = cache.get("user:" + userId);

if (profile == null) {
    profile = userRepository.findById(userId);
    cache.set("user:" + userId, profile, Duration.ofMinutes(10));
}

return profile;

Cache-aside is simple and flexible. The main downside is that the first request after a miss still pays the full database cost.

Write-through

With write-through caching, writes go through the cache and the cache writes to the underlying database.

application -> cache -> database

The benefit is that the cache is updated when data changes. The downside is added write latency because the write path includes both systems.

Write-through can work well for data that must stay warm after updates, but it requires careful failure handling. If the cache write succeeds and the database write fails, or the reverse happens, the system needs a clear recovery plan.

Write-behind

Write-behind caching writes to the cache first and persists to the database asynchronously later.

This can reduce write latency, but it increases risk. If the cache or worker fails before the database write completes, data can be lost unless the system uses durable queues or logs.

Write-behind is usually reserved for specialized high-throughput systems, not ordinary CRUD APIs.

TTLs

A time to live, or TTL, tells the cache when an item should expire.

user:42 -> expires in 10 minutes

Short TTLs improve freshness but reduce cache hit rate. Long TTLs improve hit rate but increase stale-data risk.

Choose TTLs based on the business meaning of the data:

Data typeTypical freshness need
Feature flagsOften short TTL or push invalidation.
Product catalogMinutes may be acceptable.
User permissionsShort TTL because stale permissions are risky.
Public article pagesLonger TTL is usually fine.
Financial balancesUsually avoid loose caching unless carefully designed.

TTL is not a magic answer. It is a contract about acceptable staleness.

Cache invalidation

Cache invalidation removes or updates cached data when the source changes.

Common approaches:

  • Delete the cached key after a successful write.
  • Update the cached value after a successful write.
  • Use versioned cache keys.
  • Use events to invalidate related keys.
  • Use short TTLs and accept bounded staleness.

For many backend services, deleting the key after a write is a practical starting point. The next read repopulates the cache with fresh data.

Cache stampede

A cache stampede happens when many requests miss the cache at the same time and all hit the database.

Example:

popular key expires -> 1,000 requests arrive -> 1,000 database queries

Mitigations include:

  • Add jitter to TTLs so many keys do not expire together.
  • Use request coalescing so one request repopulates the key.
  • Serve stale data briefly while refreshing in the background.
  • Warm important keys before traffic arrives.
  • Use rate limiting or backpressure around expensive rebuilds.

Read Rate Limiting in APIs Explained for related protection patterns.

Cache keys

Good cache keys are stable, specific, and include all inputs that affect the result.

For example:

user-profile:v1:user:42
search:v2:q:java-page:1-sort:relevance
account-settings:v1:account:123

Version prefixes are useful when the cached shape changes. Instead of trying to delete every old key, the application can start reading and writing a new versioned key.

What not to cache

Avoid caching data when:

  • It changes constantly.
  • Stale data is dangerous.
  • The cache key would include sensitive secrets.
  • The query is already cheap and not repeated.
  • The data is user-specific and easy to leak across users.
  • The invalidation rules are more complex than the benefit.

Caching is a performance optimization, not a replacement for correct data ownership and authorization checks.

Common mistakes

The first mistake is caching before measuring. Cache the hot path, not the path that only feels expensive.

The second mistake is using a shared key that ignores tenant, user, locale, permissions, or query parameters. That can leak data or return the wrong result.

The third mistake is assuming the cache is always available. Backend code should define what happens when the cache times out or returns an error.

The fourth mistake is caching mutable data without an invalidation plan.

Read Database Indexes Explained before caching slow queries, because a missing index may be the real fix. Read N+1 Query Problem Explained before using cache to hide repeated ORM queries, and Rate Limiting in APIs Explained for overload protection around expensive endpoints. Use the Unix Timestamp Converter when debugging cache expiration times and TTL-related logs. You can also browse the System Design Learning Path or Topics for related backend architecture guides.