Caching is one of the most important techniques in system design.
A cache stores frequently accessed data in a faster storage layer so future requests can be served without repeatedly performing an expensive operation.
The basic idea is:
Without Cache
Client
|
v
Application
|
v
Database
|
v
ResponseWith a cache:
Client
|
v
Application
|
v
Cache
|
+---- HIT ----> Response
|
+---- MISS
|
v
Database
|
v
Cache
|
v
ResponseThe key idea is:
Do expensive work once, store the result, and reuse it when possible.
Databases and other persistent systems are much slower and more expensive to access than memory.
Consider:
Memory access
↓
Very fast
Database query
↓
Much slower
Remote API call
↓
Potentially even slowerSuppose an API receives:
100,000 requests/secand every request executes the same database query.
100,000 requests
|
v
DatabaseThe database may become the bottleneck.
With caching:
100,000 requests
|
v
Cache
|
+---+---+
| |
HIT MISS
| |
v v
Response DatabaseIf the cache serves most requests, database traffic can be dramatically reduced.
Caching can improve:
Memory-based cache lookups are typically much faster than database or network operations.
The application can serve more requests without increasing database load proportionally.
A cache absorbs repeated reads.
Fewer expensive database or external-service operations may reduce infrastructure cost.
In some architectures, cached data can remain available during temporary dependency failures, if the system explicitly supports stale/fallback behavior.
Caching does not automatically guarantee availability.
Think of a cache as:
Fast temporary copy
+
Key
+
Value
+
Expiration policyExample:
Key:
user:123
Value:
{
"id": 123,
"name": "Anubhav"
}The application asks:
GET user:123If present:
CACHE HITIf absent:
CACHE MISSA cache hit means the requested data exists in the cache.
Application
|
| GET user:123
v
Cache
|
| FOUND
v
ResponseExample:
Request: user:123
Cache:
user:123 -> { ... }
Result:
HITThis is the ideal fast path.
A cache miss means the requested value is not available in the cache.
Application
|
| GET user:123
v
Cache
|
| NOT FOUND
v
DatabaseThe application then retrieves the data from the source and may populate the cache.
One of the most important cache metrics is the cache hit ratio.
Formula:
Cache Hit Ratio =
Cache Hits / Total Cache RequestsExample:
Total requests = 1,000,000
Cache hits = 900,000
Hit ratio =
900,000 / 1,000,000
= 90%Therefore:
Cache Hit Ratio = 90%The miss ratio is:
Miss Ratio = 1 - Hit RatioSo:
Miss Ratio = 10%Suppose:
Application requests = 100,000/secApproximately:
90,000 -> cache
10,000 -> databaseApproximately:
50,000 -> cache
50,000 -> databaseThe database load is five times higher in the second case.
Therefore, cache effectiveness is not just about whether a cache exists.
It is about:
How much useful traffic the cache absorbs.
Suppose:
Cache = 1 ms
Database = 30 msA cache hit may avoid most of the database latency.
Without cache:
Request
|
v
Database
|
v
30 msWith cache hit:
Request
|
v
Cache
|
v
1 msThis can significantly improve user-perceived latency.
The most common caching pattern is cache-aside.
The application manages the cache explicitly.
Flow:
1. Check cache
2. If HIT -> return
3. If MISS -> query database
4. Put result in cache
5. Return resultDiagram:
Application
|
v
Cache
|
MISS
|
v
Database
|
v
Application
|
v
CachePseudocode:
async function getUser(userId) {
const key = `user:${userId}`;
const cached = await cache.get(key);
if (cached) {
return JSON.parse(cached);
}
const user = await database.getUser(userId);
await cache.set(key, JSON.stringify(user), {
ttl: 300
});
return user;
}The database remains the source of truth.
The cache is an optimization layer.
Advantages:
Disadvantages:
With a read-through cache, the application asks the cache for data, and the cache itself retrieves missing data from the underlying source.
Conceptually:
Application
|
v
Read-Through Cache
|
+---- HIT -> return
|
+---- MISS -> DatabaseThe application does not directly implement the cache-miss database lookup.
The exact implementation depends on the caching technology.
| Property | Cache-Aside | Read-Through |
|---|---|---|
| Application checks cache | Yes | Usually no |
| Application handles miss | Yes | Cache layer |
| Control | High | More centralized |
| Complexity | Application-side | Cache infrastructure |
| Common use | Very common | Depends on platform |
The important distinction is who is responsible for loading data after a miss.
In write-through caching, writes go through the cache and are synchronously propagated to the underlying data store according to the cache system's semantics.
Conceptually:
Application
|
v
Cache
|
v
DatabaseA write updates both layers as part of the write path.
Advantages:
Disadvantages:
With write-back caching:
Application
|
v
Cache
|
| later
v
DatabaseThe cache acknowledges or stores the write first, then persists it to the underlying store later.
Advantages:
Risks:
Write-back caching should only be used when its durability behavior is explicitly understood.
In write-around caching, writes go directly to the database rather than populating the cache.
Write
|
v
DatabaseLater:
Read
|
v
Cache MISS
|
v
Database
|
v
CacheThis can prevent large volumes of write-only data from polluting the cache.
| Pattern | Write Path | Main Benefit | Main Risk |
|---|---|---|---|
| Write-through | Cache → DB | Fresh cache | Higher write latency |
| Write-back | Cache → later DB | Fast writes | Durability complexity |
| Write-around | DB only | Avoids cache pollution | First read misses |
The correct choice depends on workload and consistency requirements.
TTL means:
Time To Live
It specifies how long a cache entry remains valid.
Example:
Key:
product:123
TTL:
300 secondsAfter 300 seconds, the entry expires according to the cache's expiration semantics.
Without expiration:
Cache
|
+-- old data
+-- old data
+-- old data
+-- old dataThe cache can contain stale information indefinitely.
TTL provides automatic expiration.
Example:
Product Price
Cache:
₹999
TTL = 60 secondsAfter expiration, the next request can fetch the latest value.
TTL should depend on how quickly data changes and how much staleness is acceptable.
Examples:
| Data | Possible TTL Strategy |
|---|---|
| Product catalog | Minutes |
| Weather | Short |
| User profile | Minutes |
| Static configuration | Longer |
| Session data | Based on session policy |
| Real-time stock | Very short or explicit invalidation |
There is no universal TTL.
One of the classic engineering statements is:
There are only a few hard things in computer science: cache invalidation and naming things.
The underlying problem is simple:
Database:
price = 1200
Cache:
price = 1000The cache is stale.
When data changes, the system needs a strategy to ensure cached data eventually becomes acceptable.
Common approaches:
Suppose:
User 123is cached:
user:123A profile update happens.
The application can delete:
DELETE user:123Then the next read:
Cache MISS
|
v
Database
|
v
New value cachedThis is simple and effective for many use cases.
Another strategy:
1. Update database
2. Update cacheExample:
await db.updateUser(userId, data);
await cache.set(`user:${userId}`, JSON.stringify(data));This can reduce stale reads.
But failures can occur:
Database update = SUCCESS
Cache update = FAILURENow the cache may remain stale.
Therefore production systems need to decide how to recover from partial failures.
After a database update, you can:
DB update
|
v
DELETE cacheNext read repopulates it.
DB update
|
v
UPDATE cacheWhich is better depends on:
Deleting is often simpler because the database remains the source of truth.
A cache has finite memory.
Suppose:
Cache capacity = 10 GBEventually:
New data arrivesThe cache needs to remove something.
This is called eviction.
LRU means:
Least Recently Used
The cache removes entries that have not been accessed recently.
Example:
A -> used recently
B -> used recently
C -> not used for long time
D -> used recentlyIf space is needed:
Evict CLRU is useful when recently accessed data is likely to be accessed again.
LFU means:
Least Frequently Used
It removes entries with the lowest access frequency.
Example:
A -> 10,000 accesses
B -> 5,000 accesses
C -> 2 accessesIf C is least frequently used:
Evict CLFU can work well when popularity is stable.
| Property | LRU | LFU |
|---|---|---|
| Based on | Recent access | Frequency |
| Good for | Recency-driven workloads | Popularity-driven workloads |
| Implementation complexity | Moderate | Higher |
| Problem | Popular old item may leave | Recently trending item may initially look cold |
Modern caches may use more sophisticated policies.
A cache key must uniquely identify the cached value.
Bad:
userBetter:
user:123For parameterized data:
product:123
product:123:details
search:iphone:page:1Good keys should be:
Different data types should use clear namespaces.
Example:
user:123
order:987
product:456
session:abcThis prevents collisions.
Without namespaces:
123
456
987the meaning of a key becomes ambiguous.
Complex objects often need serialization.
Example:
const value = JSON.stringify(user);
await cache.set("user:123", value);Then:
const value = await cache.get("user:123");
const user = JSON.parse(value);Serialization has:
For high-throughput systems, serialization format and object size can matter.
A local cache lives inside the application process.
Application Server
|
+---- Local Memory CacheAdvantages:
Disadvantages:
Example:
Server A -> cache A
Server B -> cache B
Server C -> cache CA distributed cache is shared across application instances.
Application
/ | \
v v v
Server A Server B Server C
\ | /
\ | /
v v v
+-----------+
| Cache |
+-----------+Examples include systems such as Redis.
Advantages:
Disadvantages:
| Property | Local Cache | Distributed Cache |
|---|---|---|
| Latency | Very low | Low, but network-dependent |
| Shared across servers | No | Yes |
| Memory | Per application | Central/shared cluster |
| Invalidation | More difficult globally | Centralized |
| Failure impact | Usually isolated | Shared dependency |
| Scaling | Limited per instance | Designed for shared workloads |
Many production systems use both.
A two-level cache can look like:
Application
|
v
L1 Local Cache
|
MISS
|
v
L2 Distributed Cache
|
MISS
|
v
DatabaseThis can reduce network traffic to the distributed cache.
Example:
L1 = process memory
L2 = Redis
L3 = databaseBut multi-level caches increase invalidation and consistency complexity.
A cache stampede occurs when many requests simultaneously encounter an expired or missing cache entry.
Suppose:
Popular keyexpires at:
12:00:00At 12:00:01:
10,000 requests
|
v
Cache MISS
|
+----> DB
+----> DB
+----> DB
+----> DB
...The database suddenly receives a huge burst.
This can overload it.
Common techniques:
Only one request loads the missing value.
10,000 requests
|
v
One loader
|
v
DatabaseOther requests wait for the result.
Use a short-lived distributed lock around cache population.
Instead of every entry expiring at exactly the same time:
TTL = 300 + random(0, 30)This spreads expirations.
Refresh data before expiration.
A cache avalanche occurs when many cache entries become unavailable or expire around the same time.
Example:
Cache restart
|
v
Millions of misses
|
v
Database overloadMitigation:
Cache penetration occurs when requests repeatedly ask for data that does not exist.
Example:
GET user:999999999Database:
NOT FOUNDIf the system never caches the negative result:
Request
|
v
Cache MISS
|
v
Database
|
v
NOT FOUNDRepeated malicious or accidental requests can repeatedly hit the database.
A system can cache "not found" results for a short period.
Example:
user:999999999 -> NOT_FOUND
TTL = 30 secondsThen repeated requests can be served from the cache.
The TTL should be short enough that newly created data can eventually become visible.
Cache warming means populating the cache before traffic arrives.
Example:
Deployment
|
v
Load popular products
|
v
Cache
|
v
Traffic arrivesThis can reduce cold-start misses.
Useful for:
What happens if the cache is unavailable?
A common fallback is:
Application
|
v
Cache
|
FAIL
|
v
DatabaseBut there is a danger.
If millions of requests suddenly bypass the cache:
Cache failure
|
v
Database
|
v
OverloadThis is sometimes called a thundering herd or dependency-failure amplification.
Therefore cache failure handling must include:
A cache should not necessarily become a single point of failure.
Instead of:
Application
|
v
One Cache Nodeproduction systems may use:
Cache Cluster
/ | \
v v v
Node A Node B Node Cwith appropriate replication/failover behavior.
The exact design depends on the caching system.
There is often a trade-off:
Freshness
vs
PerformanceVery aggressive caching:
High performance
+
Potentially stale dataMinimal caching:
Fresh data
+
Higher database loadThe correct balance depends on business requirements.
For some data:
User profile picturea few seconds of staleness may be acceptable.
For other data:
Bank balancestale values may be unacceptable.
Therefore:
Do not ask only "Can we cache this?" Ask "How stale can this data safely be?"
Suppose:
Order Service
|
v
DatabaseAfter an update:
Order Updated Event
|
v
Cache InvalidationOther services can subscribe to the event.
Architecture:
Database Update
|
v
Event
|
+----> Cache Service
|
+----> Search Index
|
+----> AnalyticsThis becomes particularly useful in event-driven architectures.
Caching also exists at the HTTP/CDN layer.
Example:
Cache-Control: public, max-age=3600This tells compatible caches that the response may be reused for the specified period.
Other directives include concepts such as:
no-store
no-cache
private
public
must-revalidateApplication caches and HTTP/CDN caches are different layers, but they use similar caching principles.
There may be multiple cache layers:
Browser
|
v
CDN
|
v
Application Cache
|
v
DatabaseA request may be served before reaching the application at all.
This is why cache architecture should consider the complete request path.
Suppose an API caches:
/search?q=iphoneBut every tiny variation creates a new key:
/search?q=iphone&page=1
/search?q=iphone&page=2
/search?q=iphone&sort=price
/search?q=iphone&sort=ratingThe cache may contain many low-value entries.
This is called cache fragmentation or key explosion.
Possible solutions:
Cache memory is finite.
Important metrics:
A cache with high eviction rates may indicate:
A hot key receives a disproportionate number of requests.
Example:
product:iphone-17receives:
1 million requests/secEven if the cache has many nodes, one key can create a bottleneck depending on the architecture.
Potential solutions:
Suppose a cached object is:
5 MB JSONand receives:
10,000 requests/secEven if the cache lookup is fast, repeatedly:
deserialize 5 MBcan consume significant CPU and bandwidth.
Therefore cache performance is not just:
cache lookup latencyIt also includes:
network
+
serialization
+
deserialization
+
memory
+
CPUSuppose:
10 million objectsAverage object size:
2 KBRaw data:
10,000,000 × 2 KB
= approximately 20 GBBut actual cache capacity must account for:
Therefore you should not provision exactly 20 GB.
Capacity planning needs safety margin.
One of the biggest benefits of caching is database protection.
Suppose:
Requests = 1,000,000/sec
Hit ratio = 95%Then approximately:
Cache:
950,000/sec
Database:
50,000/secInstead of:
Database:
1,000,000/secThis can completely change the scalability requirements of the database.
Caching can exist at many layers:
User
|
v
Browser Cache
|
v
CDN
|
v
Reverse Proxy
|
v
Application Local Cache
|
v
Distributed Cache
|
v
Database Buffer Pool
|
v
StorageDifferent layers have different:
A strong system designer knows which layer should cache which data.
Good candidates often have:
Examples:
Product catalog
User profiles
Configuration
Feature flags
Popular content
Computed aggregations
Session data
API responsesBe cautious with:
The correct answer depends on the application's requirements.
Caches can contain sensitive data.
Potential risks:
If cache keys do not properly distinguish users:
user A -> cached response
user B -> receives user A dataNever assume:
"It came from the cache, so authentication is unnecessary."
Authorization still matters.
A response intended to be private must not accidentally become publicly cacheable.
Attackers may attempt to manipulate cached responses.
Cache security must be part of API and infrastructure design.
Suppose:
GET /products/123is requested 50,000 times/sec.
Without cache:
50,000 requests
|
v
DatabaseWith cache-aside:
50,000 requests
|
v
Redis
|
+---+---+
| |
HIT MISS
| |
v v
Response Database
|
v
RedisIf hit ratio is 98%:
Cache = 49,000/sec
Database = 1,000/secThis dramatically reduces database load.
Request:
GET /users/123Flow:
1. Check user:123
2. If hit, return profile
3. Otherwise query DB
4. Cache profile for 5 minutesOn update:
PUT /users/123Possible flow:
1. Update database
2. Delete user:123 from cacheNext read repopulates the cache.
This is a simple cache-aside design.
Build a small API with:
GET /products/:id
POST /products
PUT /products/:idUse:
Application
|
+---- Cache
|
+---- DatabaseImplement:
Cache -> HIT -> return
Cache -> MISS -> DB -> Cache -> returnDB -> create productDB -> update product
Cache -> invalidate productAdd metrics:
cache_hits
cache_misses
cache_hit_ratio
cache_errors
database_fallbacksThen simulate:
This exercise demonstrates the core production behavior of caching.
Your API receives 500,000 requests/sec. The database can safely handle only 20,000 requests/sec. How can caching help?
Suppose:
Traffic = 500,000/sec
Database capacity = 20,000/secRequired database traffic:
<= 20,000/secMaximum miss ratio:
20,000 / 500,000
= 4%Therefore the target cache hit ratio would need to be approximately:
96%+under this simplified model.
But a real design must also consider:
This is a good example of how system-design numbers connect architecture to capacity.
Use this mental model:
REQUEST
|
v
CACHE
/ \
HIT MISS
| |
v v
RESPONSE DATABASE
|
v
CACHE
|
v
RESPONSEThen ask:
1. What should we cache?
2. What is the cache key?
3. How long should it live?
4. What happens on a miss?
5. How do we invalidate it?
6. What happens if it becomes stale?
7. What happens if the cache fails?
8. What happens when many requests miss simultaneously?
9. How do we handle hot keys?
10. How much cache capacity do we need?If you can answer these questions, you understand the fundamentals of production caching.
In this chapter, we learned:
The central idea is:
Caching moves frequently needed data closer to the consumer, reducing latency and protecting slower dependencies. The difficult part is not storing data—it is deciding what to cache, how long to keep it, how to invalidate it, and how to behave when the cache fails.
Before moving forward, make sure you can explain:
Next we will go deeper into production-grade caching.
We will cover:
Mark it as complete to track your mastery.