Skip to main content

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:

  1. 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.
  2. Future requests for the same data will be faster because they are served from the cache.

Types of Caching

TypeDescription
In-Memory CacheFastest, stores data in RAM (e.g., Redis, Memcached).
Database CacheResults of DB queries cached inside or near the DB system.
Application CacheCached within the application layer (e.g., objects or method results).
Browser CacheStores static assets like HTML, CSS, JS on the client-side.
CDN CacheStores 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.