Caching
Caching is a system design strategy used to store a copy of data in a temporary storage location — known as a cache — so future requests for that data can be served faster. The primary purpose is to reduce latency, improve performance, and reduce the load on the main data source (like a database or API).
- Caching is used to optimized read operation.
- Queue is used to optimized write operation.
How Caching Works
When a client (like a browser or a backend system) requests data:
-
Check the cache:
- If the data is present (called a cache hit), it is returned immediately.
- If the data is absent (called a cache miss), the system fetches it from the main source, stores it in the cache, and then returns it.
-
Future requests for the same data will be faster because they are served from the cache.
Types of Caching
| Type | Description |
|---|---|
| In-Memory Cache | Fastest, stores data in RAM (e.g., Redis, Memcached). |
| Database Cache | Results of DB queries cached inside or near the DB system. |
| Application Cache | Cached within the application layer (e.g., objects or method results). |
| Browser Cache | Stores static assets like HTML, CSS, JS on the client-side. |
| CDN Cache | Stores static resources in geographically distributed servers (CDNs). |
Example of Cache
Suppose your system shows user profiles retrieved from a database.
Without Cache:
User Request → App Server → Database → Fetch Data → Return to User (slow)
If 1000 users request the same profile, the DB is hit 1000 times.
With Cache:
User Request → App Server → Check Redis Cache
→ [If Hit] Return Cached Data (fast)
→ [If Miss] Fetch from DB → Save to Redis → Return to User
Now, the DB is only hit once, and subsequent requests are served from Redis.
Code Example
const redis = require("redis");
const express = require("express");
const app = express();
const client = redis.createClient();
// Simulated database call
const getUserFromDB = async (id) => {
console.log("Fetching from database...");
return { id, name: "John Doe", age: 30 };
};
app.get("/user/:id", async (req, res) => {
const userId = req.params.id;
client.get(userId, async (err, cachedData) => {
if (cachedData) {
return res.send(JSON.parse(cachedData)); // Cache hit
}
const userData = await getUserFromDB(userId); // Cache miss
client.setex(userId, 3600, JSON.stringify(userData)); // Store in Redis for 1 hour
res.send(userData);
});
});
app.listen(3000, () => console.log("Server running"));
When you perform update or delete, you should also delete the cache.
Cache Invalidation & Expiry
Caching isn’t useful without a strategy to expire or update data:
- Time-based Expiry (TTL): Data expires after a certain time.
- Manual Invalidation: Delete/update cache when underlying data changes.
- LRU (Least Recently Used): Evict least-used items when cache is full.
Challenges of Caching
- Cache invalidation is hard.
- Stale data can cause inconsistencies.
- Memory constraints in cache storage.
- Complexity in managing distributed caches.