Episode 3 — NodeJS MongoDB Backend Architecture / 3.16 — Caching with Redis
3.16 — Interview Questions: Caching with Redis
Prepare for technical interviews with these 10 caching and Redis questions spanning Beginner, Intermediate, and Advanced levels, complete with model answers and a quick-fire reference table.
<< Previous: Exercise Questions | Next: Quick Revision >>
Quick-Fire Table
| # | Question | Level | Key Point |
|---|---|---|---|
| 1 | What is caching and why is it important? | Beginner | Store frequently accessed data in fast storage to reduce latency and DB load |
| 2 | What is Redis and how does it differ from a traditional database? | Beginner | In-memory data structure store, sub-millisecond access, volatile by default |
| 3 | Explain the Cache-Aside pattern | Beginner | Check cache → miss → query DB → store in cache → return |
| 4 | What are the main Redis data types? | Beginner | Strings, Lists, Sets, Sorted Sets, Hashes |
| 5 | How do you handle cache invalidation? | Intermediate | TTL-based, event-based, version-based; TTL as safety net + event-based for precision |
| 6 | How would you implement caching in an Express application? | Intermediate | Middleware that checks Redis before handler, caches response on miss |
| 7 | What is the cache stampede problem and how do you prevent it? | Intermediate | Mass cache miss on popular key expiry; use mutex lock or stale-while-revalidate |
| 8 | Redis Pub/Sub vs message queues -- when to use each? | Advanced | Pub/Sub: fire-and-forget, no persistence. Queues: reliable delivery, retries |
| 9 | How do you scale Redis for high-availability production? | Advanced | Master-replica for HA, Redis Cluster for horizontal scaling, Sentinel for failover |
| 10 | Design a rate limiter using Redis | Advanced | INCR + EXPIRE for fixed window; Sorted Sets for sliding window |
Detailed Answers
Q1. What is caching and why is it important? (Beginner)
Model Answer:
Caching is the practice of storing copies of frequently accessed data in a faster storage layer, typically memory, so that future requests can be served more quickly without repeating expensive operations like database queries or API calls.
Caching is important because:
- Performance: Memory reads are 100-1000x faster than disk-based database queries
- Scalability: Reduces database load, allowing the system to handle more concurrent users
- Cost: Fewer database queries means less infrastructure scaling needed
- User experience: Sub-millisecond responses feel instant to users
A typical web application without caching might take 50-200ms per database query. With caching, cache hits are served in under 1ms.
Q2. What is Redis and how does it differ from a traditional database? (Beginner)
Model Answer:
Redis (Remote Dictionary Server) is an open-source, in-memory data structure store. Unlike traditional databases like MongoDB or PostgreSQL that store data on disk, Redis keeps the entire dataset in RAM.
Key differences:
| Aspect | Redis | Traditional Database |
|---|---|---|
| Storage | In-memory (RAM) | On-disk |
| Speed | Sub-millisecond | Milliseconds to seconds |
| Data model | Key-value with rich types | Documents, tables, etc. |
| Persistence | Optional (RDB/AOF) | Always persistent |
| Size limit | Limited by RAM | Limited by disk |
| Primary use | Cache, sessions, queues | Primary data store |
Redis is single-threaded, which eliminates race conditions and makes all operations atomic. It supports Strings, Lists, Sets, Sorted Sets, Hashes, and more. Redis is typically used alongside a primary database, not as a replacement.
Q3. Explain the Cache-Aside pattern (Beginner)
Model Answer:
Cache-Aside, also called Lazy Loading, is the most common caching pattern where the application manages the cache explicitly:
- Check cache: When a request arrives, first check if the data exists in the cache
- Cache hit: If found, return the cached data immediately (fast path)
- Cache miss: If not found, query the database
- Store in cache: After fetching from the database, store the result in the cache with a TTL
- Return: Return the data to the caller
async function getUser(id) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached); // Cache hit
const user = await db.findUser(id); // Cache miss
await redis.set(`user:${id}`, JSON.stringify(user), { EX: 3600 });
return user;
}
Advantages: Only caches data that is actually requested; cache failures do not break the application. Disadvantages: First request always hits the database; cached data can become stale.
Q4. What are the main Redis data types? (Beginner)
Model Answer:
Redis supports five core data types:
-
Strings: The simplest type. Can hold text, numbers, or binary data up to 512MB. Commands: SET, GET, INCR. Use case: caching, counters, distributed locks.
-
Lists: Ordered collections of strings (linked list). Commands: LPUSH, RPUSH, LPOP, RPOP, LRANGE. Use case: message queues, activity feeds, recent items.
-
Sets: Unordered collections of unique strings. Commands: SADD, SMEMBERS, SINTER, SUNION. Use case: tags, unique visitors, online users, common friends.
-
Sorted Sets: Like Sets but each member has a score for ordering. Commands: ZADD, ZRANGE, ZREVRANGE, ZRANK. Use case: leaderboards, rankings, priority queues.
-
Hashes: Maps of field-value pairs, like a mini-object. Commands: HSET, HGET, HGETALL, HINCRBY. Use case: user profiles, session data, object caching.
Each type has specialized commands optimized for its use case, which makes Redis much more powerful than a simple key-value store.
Q5. How do you handle cache invalidation? (Intermediate)
Model Answer:
Cache invalidation ensures cached data stays in sync with the source of truth. There are several strategies:
-
TTL-based: Set an expiration time on every cache entry. After the TTL expires, the next request triggers a fresh database query. This is the simplest approach and should always be used as a safety net.
-
Event-based: When data is modified (create, update, delete), explicitly delete or update the corresponding cache entries. This provides immediate consistency but requires knowing all affected cache keys.
-
Version-based: Include a version number in the cache key. When data changes, increment the version. Old cache entries naturally expire.
-
Write-through: Update the cache whenever you update the database, keeping them always in sync.
In practice, I use a combination: TTL as a safety net (e.g., 30 minutes) combined with event-based invalidation on writes for immediate consistency. The TTL ensures that even if an invalidation event is missed, stale data eventually expires.
The tricky part is identifying all cache keys affected by a write. For example, updating a product requires invalidating both product:{id} and any list caches like products:category:electronics.
Q6. How would you implement caching in an Express application? (Intermediate)
Model Answer:
I would implement a reusable cache middleware that transparently caches route responses:
- Redis client module: Singleton connection with error handling and reconnection
- Cache middleware: Checks Redis before the route handler executes. If cached data exists, returns it immediately. If not, intercepts
res.json()to cache the response before sending it - Cache invalidation: In write operations (POST, PUT, DELETE), delete relevant cache keys
- Graceful degradation: If Redis is unavailable, the middleware catches the error and passes the request to the handler normally
function cacheMiddleware(prefix, ttl) {
return async (req, res, next) => {
try {
const key = `${prefix}:${req.originalUrl}`;
const cached = await redis.get(key);
if (cached) return res.json(JSON.parse(cached));
const originalJson = res.json.bind(res);
res.json = (body) => {
redis.set(key, JSON.stringify(body), { EX: ttl }).catch(() => {});
return originalJson(body);
};
next();
} catch { next(); } // Fail open
};
}
Key considerations: cache key design (include query params, user ID for personalized data), only cache successful responses, and proper invalidation on mutations.
Q7. What is the cache stampede problem and how do you prevent it? (Intermediate)
Model Answer:
A cache stampede (also called thundering herd) occurs when a popular cache entry expires and many concurrent requests all experience a cache miss simultaneously. All these requests hit the database at the same time, potentially overwhelming it.
For example, if a product page cached at key product:popular expires and 500 requests arrive in the same second, all 500 will query the database.
Prevention strategies:
-
Mutex/Lock: The first request to encounter a cache miss acquires a lock and fetches from the database. Other requests wait for the lock to release and then read from the newly populated cache. Implementable with
SET lock:key value NX EX 30. -
Stale-While-Revalidate: Keep a "stale" copy of the data with a longer TTL. When the primary cache expires, serve the stale data immediately while refreshing the cache in the background.
-
Early expiration/jitter: Add random jitter to TTL values so that cache entries for similar data do not all expire at the same time. Instead of
TTL = 3600, useTTL = 3600 + random(0, 300). -
Cache warming: Proactively refresh popular cache entries before they expire using a background job.
Q8. Redis Pub/Sub vs message queues -- when to use each? (Advanced)
Model Answer:
Redis Pub/Sub and message queues serve different purposes:
Redis Pub/Sub:
- Fire-and-forget: messages are delivered to currently connected subscribers only
- No persistence: if a subscriber is offline, it misses the message
- No acknowledgment: no guarantee of processing
- One-to-many: all subscribers on a channel receive every message
- Very fast, minimal overhead
- Use cases: real-time notifications, cache invalidation across servers, live updates
Message Queues (Redis Streams, RabbitMQ, etc.):
- Reliable delivery: messages persist until consumed and acknowledged
- At-least-once processing: failed messages can be retried
- One-to-one (typically): each message is processed by one consumer
- Consumer groups: load balance messages across multiple workers
- Use cases: job processing, order fulfillment, email sending, any task that must not be lost
Choose Pub/Sub when losing an occasional message is acceptable and you need real-time broadcast. Choose a message queue when every message must be processed reliably.
Q9. How do you scale Redis for high-availability production? (Advanced)
Model Answer:
Redis scaling involves three dimensions:
-
Redis Sentinel (High Availability):
- Monitors master and replica nodes
- Automatically promotes a replica to master if the master fails
- Notifies clients of the new master
- Provides automatic failover, not scaling
-
Master-Replica Replication (Read Scaling):
- One master handles all writes
- Multiple replicas asynchronously copy data and handle reads
- Read-heavy workloads distribute across replicas
- Replicas can be in different geographic regions
-
Redis Cluster (Horizontal Scaling):
- Data is automatically sharded across multiple nodes using 16,384 hash slots
- Each node handles a subset of the keyspace
- Built-in replication for each shard
- Scales both reads and writes
- Trade-off: multi-key operations limited to same hash slot
For most applications, I would start with a single Redis server (handles 100,000+ ops/sec), add replicas for read scaling and failover, and move to Redis Cluster only when a single server's memory is insufficient.
Q10. Design a rate limiter using Redis (Advanced)
Model Answer:
I would implement two approaches:
Fixed Window (simpler):
Key: ratelimit:{user_ip}:{current_minute}
INCR key → count
If count == 1: EXPIRE key 60
If count > limit: return 429
Sliding Window (more accurate): Uses a Sorted Set with timestamps as scores:
Key: ratelimit:sliding:{user_ip}
1. ZREMRANGEBYSCORE key -inf (now - window) → remove old entries
2. ZADD key {now} {unique_id} → add current request
3. ZCARD key → count requests
4. EXPIRE key {window} → auto-cleanup
5. If count > limit: return 429
The fixed window is simpler but has a boundary problem: a user could make limit requests at 11:59:59 and another limit at 12:00:01, effectively doubling the rate. The sliding window avoids this by always looking at the exact trailing window.
Both approaches should: set response headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset), fail open if Redis is unavailable (do not block legitimate traffic), and use appropriate limits per endpoint (stricter for login, lenient for reads).