Cache in Software System Design — A Practical Guide

By Oleksandr Andrushchenko — Published on — Modified on

Caching is one of the most effective techniques for improving performance, scalability, and availability in software systems. Instead of repeatedly fetching data from a slower source such as a database, API, object storage, or another service, a cache stores frequently used data in a faster layer.

This article explains where caches fit in system design, common caching strategies, invalidation techniques, trade-offs, failure modes, and practical production recommendations.

Table of Contents

Caching Fundamentals

What is Caching?

Caching is the process of temporarily storing frequently accessed or expensive-to-compute data in a faster storage layer. Instead of recomputing data or repeatedly fetching it from a database, external API, object storage, or another service, the application can serve it quickly from memory, edge nodes, or nearby storage.

For example, an e-commerce product page may read product details, prices, images, reviews, and recommendations from multiple systems. Without caching, every page view may trigger many expensive reads. With caching, frequently requested product data can be served from Redis, CDN, or application memory, reducing latency and load on backend systems.

User Request
  |
Application
  |
Check Cache
  |
+-------------------+
|                   |
Cache Hit           Cache Miss
|                   |
Return data         Load from database
                    Save to cache
                    Return data

Why Caching Matters

Caching matters because many systems are read-heavy. Users often request the same pages, objects, configurations, profiles, feeds, reports, or images many times. Caching reduces repeated work and helps systems handle more traffic without constantly scaling the database or expensive backend services.

Benefit How Caching Helps Example
Performance Serves data from faster storage Redis response in milliseconds instead of a slow database query
Scalability Reduces repeated backend load Product catalog served from cache during a sale
Cost efficiency Reduces computation, database reads, and API calls Cache expensive analytics summaries
Availability Can serve stale or cached data during partial outages CDN serves cached article while origin is down
User experience Improves perceived speed Profile page loads quickly from cached data

When Caching Helps Most

Caching is most useful when data is read frequently, expensive to compute, expensive to fetch, or acceptable to serve slightly stale. It is less useful for data that changes constantly, requires strict correctness, or is rarely reused.

Good Candidate for Cache Poor Candidate for Cache
Product catalog Real-time payment state
Public articles and images One-time unique query result
User permissions with short TTL Rapidly changing inventory without careful invalidation
Configuration values Strongly consistent financial balance
Analytics summaries Data that is cheap and fast to fetch directly

Rule of thumb: cache data when repeated reads are more expensive than the complexity of keeping the cached value reasonably fresh.

Cache Layers

Client-Side Cache

Client-side caching stores data in the browser, mobile app, or desktop client. Examples include browser HTTP cache, localStorage, IndexedDB, service workers, and mobile app storage. This is useful for static assets, user preferences, offline support, and data that does not need immediate freshness.

For example, a web application may cache CSS, JavaScript, fonts, and images in the browser. A mobile app may cache recent feed items so users can still see some content with poor network connectivity.

CDN Cache

CDN caching stores content at edge locations close to users. CDNs are commonly used for images, JavaScript, CSS, videos, public pages, and sometimes API responses. CDN caching improves latency and can keep content available even when the origin is temporarily unavailable.

User
  |
Nearest CDN Edge
  |
Origin Server
  |
Database

Application-Level Cache

Application-level caching stores computed results, query results, sessions, permissions, or hot objects in memory or a distributed cache such as Redis or Memcached. This is one of the most common caching layers in backend systems because it protects databases from repeated reads.

For example, an API can cache user profile data, permission checks, feature flags, or expensive dashboard summaries. This reduces database load and improves response time for repeated requests.

Database Cache

Databases already use internal caches such as buffer pools, page caches, query plan caches, and indexes. These caches help reduce disk access and speed up repeated operations. However, database cache alone may not be enough when the same expensive queries are executed repeatedly by the application.

Operating System Cache

Operating systems cache file system pages and disk blocks in memory. This helps speed up repeated reads from disk. It is usually invisible to the application, but it can significantly affect performance benchmarks and production behavior.

Cache Layer Comparison

Cache Layer Closest To Best For Main Risk
Client cache User device Static assets, offline data Hard to invalidate immediately
CDN cache Geographic edge Public content, images, pages Stale content at edge
Application cache Backend service Computed data, hot objects Invalidation complexity
Distributed cache Shared backend layer Sessions, counters, shared objects Network dependency and memory pressure
Database cache Database engine Indexes, pages, query plans Limited by database capacity

Cache Placement in Architecture

Multi-Layer Cache Architecture

Cache placement determines what problem the cache solves. Edge caching reduces global latency. Application caching reduces repeated backend work. Database caching reduces disk access. A well-designed system often uses multiple cache layers, but each layer should have a clear purpose.

User
  |
Browser Cache
  |
CDN Cache
  |
Load Balancer
  |
Application
  |
Redis / Memcached
  |
Database
  |
Disk / Storage Cache

Edge Caching

Edge caching stores content close to users through a CDN. This is ideal for static files, public pages, images, documentation, blog posts, and cacheable API responses. For example, a system design blog can serve article pages and images from a CDN, reducing load on the origin application.

Edge caching is especially effective when many users request the same public content. The main trade-off is freshness: once content is distributed across edge locations, it may take time or explicit invalidation to update everywhere.

Mid-Tier Caching

Mid-tier caching uses reverse proxies, API gateways, or service-level caches between clients and backend systems. This can reduce repeated API calls and protect backend services from spikes.

For example, an API gateway can cache public catalog responses for a short time. This prevents every request from reaching the application layer while still keeping data reasonably fresh.

Data Caching

Data caching stores objects, query results, serialized responses, counters, sessions, or computed aggregates close to the application. This is useful when the database is becoming a bottleneck or when expensive computations are repeated often.

Placement Primary Goal Example
Browser Reduce network calls Cache static assets
CDN Reduce global latency Serve images and public pages
API gateway Protect backend services Cache public API responses
Application service Reduce repeated computation Cache permissions or dashboard summaries
Database Reduce disk reads Buffer pool and index pages

Caching Strategies

Cache-Aside

Cache-aside, also called lazy loading, is one of the most common caching patterns. The application checks the cache first. If the data is missing, the application loads it from the database, stores it in the cache, and returns it.

Cache-aside
Cache-aside
data = cache.get(key)

if data is None:
    data = database.query(key)
    cache.set(key, data, ttl=300)

return data

Cache-aside is simple and flexible, but the first request after expiration is slower because it must load data from the source. It works well for product details, user profiles, public pages, and computed summaries.

Read-Through

Read-through caching makes the cache responsible for loading missing data from the source. The application only talks to the cache. On a cache miss, the cache loads the data, stores it, and returns it.

Read-through cache
Read-through cache

This pattern simplifies application code but requires cache infrastructure or libraries that understand how to load data from the source. It is useful when many services need the same loading behavior.

Write-Through

Write-through caching writes data to the cache and the database in the same operation. This keeps the cache fresh because updates pass through the cache layer before being persisted.

Write-through cache
Write-through cache

Write-through improves consistency between cache and database, but write latency can increase because each write must update both systems. It is useful when reads are frequent and the cache should be immediately updated after writes.

Write-Behind

Write-behind, also called write-back caching, writes data to the cache first and persists it to the database asynchronously later. This can improve write latency and absorb write spikes, but it increases the risk of data loss if the cache fails before persistence.

Write-behind cache
Write-behind cache

This pattern should be used carefully for critical data. It may be acceptable for analytics counters, logs, temporary state, or non-critical event buffers, but it is risky for payments, orders, or financial records unless durability is guaranteed.

Refresh-Ahead

Refresh-ahead refreshes cache entries before they expire. This avoids latency spikes when popular keys expire and many users request the same data at the same time.

Refresh-ahead cache
Refresh-ahead cache

Refresh-ahead works well for predictable hot data, such as homepage content, product catalogs, trending feeds, exchange rates, or dashboard summaries. The trade-off is extra background work to refresh data that may not always be requested.

Strategy Comparison

Strategy Best For Advantages Trade-offs
Cache-aside General application caching Simple, flexible, widely used Cache miss latency, possible stale data
Read-through Centralized loading logic Simpler application code Requires cache loader support
Write-through Data that must stay fresh in cache Better consistency Higher write latency
Write-behind High-write workloads, counters, logs Fast writes, absorbs spikes Risk of data loss or delayed persistence
Refresh-ahead Predictable hot keys Avoids miss latency spikes Extra background refresh work

Cache Correctness

Cache Invalidation

Cache invalidation ensures cached data does not become dangerously outdated. It is one of the hardest parts of caching because the cache and source of truth must stay reasonably synchronized while still preserving performance benefits.

There is no universal invalidation strategy. A blog post, product image, feature flag, account permission, and payment status all have different correctness requirements. The stricter the correctness requirement, the more carefully invalidation must be designed.

TTL-Based Invalidation

TTL, or time to live, expires entries after a fixed time. This is the simplest invalidation method and works well when some staleness is acceptable.

cache.set("product:123", product_data, ttl=300)

After 300 seconds, the key expires automatically.

TTL-based invalidation is often enough for public content, catalog pages, configuration values, and dashboards. The main weakness is that data may remain stale until the TTL expires.

Event-Based Invalidation

Event-based invalidation removes or updates cache entries when the source data changes. For example, when a product price changes, the product service can publish an event that invalidates the related cache keys.

Product Updated
  |
Publish Event
  |
Cache Invalidator
  |
Delete product:123
Delete category:shoes:page:1

This keeps cache entries fresher than TTL alone, but it introduces more moving parts. The system must handle missed events, duplicate events, delayed events, and partial invalidation failures.

Version-Based Invalidation

Version-based invalidation includes a version number, timestamp, or content hash in the cache key. When the data changes, the version changes, and the application naturally reads from a new key.

Old key:
post:123:v1

New key:
post:123:v2

This is useful for CDNs, static assets, articles, configuration, and systems where deleting old cache entries everywhere is difficult. Old keys can remain until they expire or are evicted.

Manual Invalidation

Manual invalidation means explicitly clearing cache entries through admin tools, deployment scripts, or operational commands. This is useful for emergency fixes, content publishing, and rare correction workflows, but it should not be the primary strategy for high-frequency updates.

Invalidation Strategy Comparison

Strategy Best For Pros Cons
TTL Data that tolerates temporary staleness Simple and reliable Data may be stale until expiration
Event-based Frequently updated important data Fresher cache More moving parts
Version-based Static assets, articles, config Avoids hard deletes Old keys may remain until eviction
Manual Emergency fixes and admin workflows Direct control Not scalable for frequent changes

Cache Design

Cache Keys and Granularity

Cache keys uniquely identify cached data. Poor key design can cause collisions, stale data, incorrect user data exposure, and low hit ratios. Good keys are descriptive, stable, namespaced, and include all important dimensions of the cached result.

Good Cache Key Patterns

user:123:profile
user:123:permissions:v4
post:456:comments:page:2
product:789:details:currency:USD
tenant:42:dashboard:2026-06-04
prod:api:v1:homepage
  • Namespace by environment: use prefixes such as prod:, staging:, or dev:.
  • Include tenant or user scope: avoid leaking data between customers.
  • Include query parameters: page, filters, locale, currency, and permissions may change the result.
  • Avoid huge values: smaller composable objects are often easier to invalidate and reuse.
  • Use versioning when needed: include schema, content, or permission versions in keys.

Granularity Trade-off

Cache granularity controls how much data each key stores. Larger cached objects can make responses very fast, but they are harder to invalidate. Smaller cached objects are more precise, but they may require more cache lookups and more application-side composition.

Granularity Example Benefit Trade-off
Large object Full homepage response Very fast response Harder invalidation
Medium object Product details block Reusable across pages More application composition
Small object Single user permission value Precise invalidation More cache lookups

Eviction Policies

Eviction happens when the cache removes entries to free memory or because entries expired. Eviction policy determines which keys are removed first. The right policy depends on access patterns and memory constraints.

Policy Meaning Best For
LRU Least recently used items are removed first Workloads where recent access predicts future access
LFU Least frequently used items are removed first Workloads with stable hot keys
FIFO Oldest items are removed first Simple queue-like expiration
TTL-based Items expire after a fixed time Data with known freshness requirements
Random Random keys are removed Simple systems where precision is not required

Production Cache Design

Cache Stampede

A cache stampede happens when many requests miss the same key at the same time and all try to recompute or reload the value from the database. This can overload the origin system.

Popular key expires
  |
Thousands of requests miss cache
  |
All requests query database
  |
Database overload

Common solutions include request coalescing, locks, stale-while-revalidate, refresh-ahead, jittered TTLs, and serving stale data while one process refreshes the key.

Cache Penetration

Cache penetration happens when requests repeatedly ask for data that does not exist. Since the cache has no value for these keys, every request hits the database. This can happen because of bugs, scraping, or malicious traffic.

Common solutions include negative caching, Bloom filters, validation, and rate limiting.

Cache Avalanche

A cache avalanche happens when many keys expire at the same time, causing a sudden spike in database traffic. This often happens when many keys are written with the same TTL.

Common solutions include TTL jitter, staggered expiration, refresh-ahead, and warming important keys before major traffic events.

Cold Start

A cold cache has little or no useful data. This can happen after deployment, cache restart, failover, or scaling up a new cache cluster. Cold starts increase latency and backend load until the cache warms up.

For example, after a Redis cluster restart, a product catalog API may suddenly send all reads to the database. If the database was sized assuming cache protection, it may become overloaded.

Cache Outage

If the application depends too heavily on the cache, a cache outage can become a system outage. Critical systems should define whether the cache is optional or required. For many read caches, the application should be able to bypass the cache and query the database directly, possibly with rate limits.

Failure Mode Risk Mitigation
Stampede Database overload after hot key expires Locks, request coalescing, stale-while-revalidate
Penetration Repeated misses for nonexistent data Negative caching, Bloom filters, validation
Avalanche Many keys expire together TTL jitter, staggered expiration
Cold start Sudden backend load after empty cache Warmup, gradual traffic, preloading hot keys
Cache outage Application cannot read cached data Fallback path, rate limits, degraded mode

Observability and Metrics

Important Cache Metrics

Caching should be measured. Without metrics, it is hard to know whether the cache is helping, wasting memory, hiding database problems, or serving stale data.

Metric What It Shows Why It Matters
Hit ratio Hits divided by total cache lookups Shows whether the cache is effective
Miss ratio Misses divided by total lookups High misses may overload origin
Eviction rate How often keys are removed due to memory pressure High evictions may mean cache is too small
Memory usage Used memory and fragmentation Prevents crashes and excessive eviction
Hit latency Latency when cache contains the value Should be much faster than origin
Miss latency Latency when loading from source Shows user impact of misses
Key churn How fast keys are created and removed High churn can reduce cache efficiency

Hit Ratio Warning

A high hit ratio is not always enough. A cache can have a high hit ratio while still failing critical requests. For example, caching many cheap objects may produce good metrics, while expensive checkout or permission requests still miss frequently. Measure cache effectiveness by business-critical paths, not only globally.

For example, a system may report a 95% cache hit ratio because images and public pages are cached well. But if the checkout API misses cache on every permission check and overloads the database, users still experience failures. Measure cache performance by critical workflow, not only by global average.

Real-World Examples

E-Commerce Product Page

An e-commerce product page may cache product details, images, category metadata, reviews, and recommendations. However, inventory and price may require shorter TTLs or event-based invalidation because stale data can create bad user experiences.

Data Cache Strategy Reason
Product images CDN cache with long TTL Rarely change and are expensive to serve repeatedly
Product description CDN or Redis with versioning Changes occasionally
Price Short TTL or event invalidation Must not be stale for too long
Inventory Very short TTL or direct read Incorrect stock creates failed orders
Recommendations Cache-aside or refresh-ahead Can tolerate temporary staleness

Social Media Feed

A social media feed may cache timelines, trending posts, user profiles, media, and engagement counters. Some values, such as likes and views, can be eventually consistent. Other values, such as privacy settings, need stricter invalidation.

User opens feed
  |
Read cached timeline
  |
Fetch missing posts
  |
Load media from CDN
  |
Update view counters asynchronously

This design keeps the feed fast while allowing counters and analytics to update later. Privacy and blocking rules should be handled more carefully because stale access-control data can expose content incorrectly.

Analytics Dashboard

Analytics dashboards often cache expensive aggregations. For example, daily revenue, active users, conversion rate, or campaign performance can be precomputed and cached. Users usually accept a dashboard that is delayed by a few minutes if it loads quickly and consistently.

Dashboard Data Recommended Cache Typical Freshness
Daily revenue Precomputed aggregate cache 1-15 minutes
Active users Redis or materialized view 1-5 minutes
Historical reports Long TTL cache Hours or days
Live incident metrics Short TTL or direct stream Seconds

Production Recommendations

  • Start with simple TTL-based cache-aside. It is easy to understand, test, and operate.
  • Cache read-heavy and expensive data first. Do not cache everything by default.
  • Choose TTL based on business correctness. Product descriptions can live longer than inventory or permissions.
  • Use external caches for distributed systems. Redis or Memcached is usually better than local memory when multiple app instances need shared cache.
  • Protect the database from stampedes. Use locks, request coalescing, refresh-ahead, stale-while-revalidate, or TTL jitter.
  • Design cache keys carefully. Include tenant, user, locale, currency, permissions, filters, and version when they affect the result.
  • Do not make cache the only source of truth unless designed for it. For critical data, the database or durable storage should remain authoritative.
  • Monitor hit ratio, evictions, latency, and memory usage. Caching without observability is risky.
  • Document keys, TTLs, and invalidation rules. Future engineers need to understand why cached data behaves the way it does.

Conclusion

Caching is a powerful system-design tool, but it is not just a performance trick. It affects scalability, availability, consistency, cost, and operational complexity.

For most systems, the best approach is to start with simple cache-aside caching, short and safe TTLs, clear key naming, and strong observability. Add event-based invalidation, refresh-ahead, write-through, or write-behind only when the workload and correctness requirements justify the extra complexity.

Key takeaway: cache data that is expensive to fetch or compute, read frequently, and safe to serve slightly stale. Avoid caching critical data without a clear invalidation and recovery strategy.

Comments (0)