Episode 3 — NodeJS MongoDB Backend Architecture / 3.16 — Caching with Redis

3.16 — Exercise Questions: Caching with Redis

Practice your understanding of caching concepts, Redis commands, node-redis integration, and advanced Redis features with these 30+ exercises.


<< Previous: 3.16.d — Advanced Redis Features | Next: Interview Questions >>


Caching Fundamentals (Questions 1-8)

Q1. Explain the difference between a cache hit and a cache miss. What is a hit ratio, and why does it matter?

Hint: A cache hit serves data from memory (~0.5ms), a cache miss requires a database query (~50ms). Hit ratio = hits / (hits + misses). Aim for 80-95%+. Higher hit ratios mean fewer database queries and faster responses.


Q2. Describe the Cache-Aside (Lazy Loading) pattern step by step. Write pseudocode for a function that uses this pattern to fetch a user profile.

Hint: 1) Check cache, 2) If found (hit), return cached data, 3) If not found (miss), query database, 4) Store result in cache with TTL, 5) Return data. The cache is only populated when data is actually requested.


Q3. Compare Write-Through and Write-Behind caching strategies. When would you use each? What are the trade-offs?

Hint: Write-Through writes to both cache and DB synchronously (consistent but slower writes). Write-Behind writes to cache first, then DB asynchronously (fast writes but risk of data loss). Use Write-Through when consistency matters; Write-Behind for high-write throughput.


Q4. You have an e-commerce product catalog with 50,000 products. Products are updated about 10 times per day, but the catalog page receives 100,000 views per day. What TTL would you choose for caching the product list, and why?

Hint: With a 10,000:1 read-to-write ratio, this is an excellent caching candidate. A TTL of 5-15 minutes works well -- frequent enough to pick up changes reasonably quickly, but long enough to absorb the vast majority of reads. Event-based invalidation on product updates would be even better.


Q5. What is cache invalidation? Why is it considered one of the hardest problems in computer science? List four invalidation strategies.

Hint: Cache invalidation is ensuring cached data stays in sync with the source. It is hard because determining when data is stale requires understanding all write paths. Strategies: TTL-based, event-based, version-based, manual invalidation.


Q6. Explain the cache stampede problem. What happens when a popular cache entry expires? Describe two solutions.

Hint: When a popular key expires, hundreds of simultaneous requests all miss the cache and hit the database at once. Solutions: (1) Mutex/lock -- only one request fetches from DB while others wait, (2) Stale-while-revalidate -- serve stale data while refreshing in background.


Q7. List three types of data that are good candidates for caching and three types that are bad candidates. Explain your reasoning for each.

Hint: Good: read-heavy/infrequently changing data (product catalogs), expensive computations (dashboard statistics), third-party API responses (rate-limited APIs). Bad: rapidly changing data (stock prices), write-heavy data (real-time logs), unique-per-request data (one-time tokens).


Q8. What is the difference between local in-memory caching (e.g., node-cache) and distributed caching (e.g., Redis)? When would you use each?

Hint: Local caching is process-specific, lost on restart, no sharing between servers. Redis is shared across processes/servers, optionally persistent, supports rich data types. Use local for single-server apps with simple needs; Redis for multi-server, production environments.


Redis Basics (Questions 9-15)

Q9. Name the five core Redis data types. For each, give one real-world use case and the primary commands used.

Hint: Strings (caching, SET/GET), Lists (queues, LPUSH/RPOP), Sets (unique visitors, SADD/SMEMBERS), Sorted Sets (leaderboards, ZADD/ZRANGE), Hashes (user profiles, HSET/HGETALL).


Q10. Explain the difference between RDB and AOF persistence in Redis. When would you use each? What does Redis recommend for production?

Hint: RDB = point-in-time snapshots (compact, fast restart, possible data loss between snapshots). AOF = append-only log of every write (minimal data loss, larger files, slower restart). Production recommendation: use both together for durability and fast backups.


Q11. Write the Redis CLI commands to: (a) store a user name with a 1-hour TTL, (b) check the remaining TTL, (c) increment a page view counter, (d) store a user profile as a Hash with name, email, and age fields.

Hint: (a) SET user:name "Alice" EX 3600, (b) TTL user:name, (c) INCR pageviews:home, (d) HSET user:1001 name "Alice" email "alice@example.com" age "30".


Q12. Why should you never use the KEYS * command in production? What should you use instead?

Hint: KEYS scans the entire keyspace and blocks the single-threaded Redis server. On large datasets, this can freeze Redis for seconds. Use SCAN instead, which iterates incrementally without blocking.


Q13. Compare Redis and Memcached. List at least five differences. Why is Redis generally preferred for Node.js applications?

Hint: Redis has rich data types (Memcached: strings only), persistence (Memcached: none), Pub/Sub (Memcached: none), transactions (Memcached: none), built-in replication (Memcached: none). Redis is preferred because it serves as cache, session store, message broker, and queue in one tool.


Q14. What are Redis eviction policies? Explain allkeys-lru and volatile-ttl. Which would you recommend for a pure caching use case?

Hint: Eviction policies determine what to remove when memory is full. allkeys-lru removes the least recently used key from all keys. volatile-ttl removes the key with the shortest remaining TTL. For pure caching, allkeys-lru is recommended because all keys are candidates for eviction.


Q15. Draw (or describe in text) the architecture diagram for a Redis master-replica setup. Why would you use replicas?

Hint: Master handles all writes; replicas asynchronously copy data from master and handle reads. Benefits: read scaling (distribute reads across replicas), high availability (promote replica to master if master fails), geographic distribution.


Node.js + Redis (Questions 16-22)

Q16. Write the code to create a reusable Redis client module using node-redis v4+ with error handling, reconnection strategy, and a clean close function.

Hint: Use createClient(), set up on('error') and on('ready') handlers, implement a singleton pattern, configure reconnectStrategy in socket options, export getClient() and close() functions.


Q17. Write a function cacheQuery(key, queryFn, ttl) that implements the cache-aside pattern with JSON serialization and graceful degradation (falls back to database if Redis is unavailable).

Hint: Try to get from Redis, parse JSON on hit. On miss, call queryFn(), stringify result, SET with EX. Wrap Redis operations in try/catch so database still works if Redis is down.


Q18. Create an Express middleware function that caches GET request responses. It should build cache keys from the URL and only cache successful (2xx) responses.

Hint: Override res.json to intercept the response body. Check res.statusCode before caching. Use req.originalUrl for the cache key. Wrap in try/catch for graceful degradation.


Q19. Write the cache invalidation code for a blog post CRUD. When a post is created, updated, or deleted, which cache keys need to be invalidated?

Hint: On create: invalidate list caches (posts:all:*). On update: invalidate specific post cache (post:{id}) AND list caches. On delete: same as update. Use SCAN to find keys matching patterns.


Q20. Why must you serialize objects with JSON.stringify() before storing in Redis? What happens if you store a JavaScript object directly?

Hint: Redis stores strings. Storing an object directly converts it to "[object Object]". You must JSON.stringify() before SET and JSON.parse() after GET. Alternatively, use Hashes for flat objects.


Q21. Write code that stores a user profile as a Redis Hash (not a JSON string). Show how to read a single field, read all fields, and increment a numeric field atomically.

Hint: client.hSet('user:1', { name: 'Alice', views: '0' }), client.hGet('user:1', 'name'), client.hGetAll('user:1'), client.hIncrBy('user:1', 'views', 1).


Q22. Explain the importance of graceful degradation in a caching layer. Write a wrapper function that tries Redis first and falls back to the database, logging warnings when Redis is unavailable.

Hint: The cache should never break the application. Wrap Redis calls in try/catch. On error, log a warning and proceed to the database. Also try to cache the result non-blocking (catch and ignore errors on the SET as well).


Advanced Redis (Questions 23-30)

Q23. Write a simple job queue using Redis Lists. The producer pushes jobs with LPUSH, and the worker consumes them with BRPOP (blocking pop).

Hint: Producer: client.lPush('queue:jobs', JSON.stringify(job)). Worker: loop with client.brPop('queue:jobs', 0) which blocks until a job is available. Parse the JSON and process the job.


Q24. Implement an online user tracker using Redis Sets. Write functions for userOnline(id), userOffline(id), isOnline(id), onlineCount(), and onlineFriends(userId) (intersection of friends and online users).

Hint: sAdd('online', id), sRem('online', id), sIsMember('online', id), sCard('online'), sInter(['friends:{userId}', 'online']).


Q25. Build a real-time leaderboard using Sorted Sets. Write functions to add/update a player's score, get the top 10, get a player's rank, and get players within a score range.

Hint: zAdd or zIncrBy for scores, zRange with REV for top players, zRevRank for rank, zRangeByScore for score ranges.


Q26. Explain Redis Pub/Sub. Write a publisher that sends order events and a subscriber that processes them. Why do you need separate Redis clients for subscribing?

Hint: Publisher: client.publish('orders', JSON.stringify(data)). Subscriber: client.subscribe('orders', callback). Separate clients needed because a subscribed client enters a special state and cannot execute other commands.


Q27. Write a Redis transaction (MULTI/EXEC) that transfers points between two users atomically. What happens if one command in the transaction fails?

Hint: client.multi().hIncrBy(fromKey, 'points', -amount).hIncrBy(toKey, 'points', amount).exec(). Commands are queued and executed atomically. If one fails, Redis does NOT roll back others (unlike SQL). Use WATCH for optimistic locking.


Q28. Configure Express sessions with connect-redis. Write the setup code including the Redis client, session middleware options, login route, and logout route with session destruction.

Hint: Create RedisStore from connect-redis, pass the Redis client. Configure express-session with the store, secret, cookie options. Login: req.session.userId = user.id. Logout: req.session.destroy().


Q29. Implement a fixed-window rate limiter using Redis INCR and EXPIRE. Create an Express middleware that limits each IP to 100 requests per minute and returns 429 status with appropriate headers when exceeded.

Hint: Key: ratelimit:{ip}:{currentMinute}. INCR the key, EXPIRE on first request. Check if count exceeds limit. Set X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers.


Q30. Compare the fixed-window and sliding-window rate limiting approaches. Implement a sliding-window rate limiter using Sorted Sets.

Hint: Fixed window: simple INCR with TTL, but allows burst at window boundary (up to 2x limit). Sliding window: use ZADD with timestamp as score, ZREMRANGEBYSCORE to remove old entries, ZCARD to count. More accurate but slightly more expensive.


Bonus Questions (31-33)

Q31. Design a complete caching architecture for a social media feed API. The feed shows posts from followed users, sorted by time. Consider: cache key design, TTL strategy, invalidation when a new post is created, pagination, and personalization (each user sees a different feed).

Hint: Per-user feed cache with key feed:{userId}:page:{n}. Use Redis Lists to prepend new posts. Invalidate follower feeds when a user posts (fan-out). Short TTL (2-5 min) as a safety net. Consider hybrid: cache recent posts in Redis Sorted Sets with timestamp scores.


Q32. You notice your Redis memory usage is growing beyond the configured maxmemory. Diagnose the issue: what commands would you use to investigate? What are potential causes and solutions?

Hint: Use INFO memory for memory stats, DBSIZE for key count, SCAN with TYPE to find large keys, MEMORY USAGE key for individual key size. Causes: missing TTLs, TTLs too long, storing large values. Solutions: add/reduce TTLs, set maxmemory-policy, compress values, use Hashes instead of JSON strings.


Q33. Write a complete Express application with: (1) MongoDB for data storage, (2) Redis for caching with cache-aside pattern, (3) Session management with connect-redis, (4) Rate limiting middleware, and (5) Cache invalidation on writes. Include proper error handling and graceful degradation.

Hint: This is a synthesis exercise combining all concepts. Structure: config/redis.js (client module), middleware/cache.js (caching middleware), middleware/rateLimiter.js (rate limiting), session setup in app.js, service layer with cache-aside pattern, and invalidation in write operations.


<< Previous: 3.16.d — Advanced Redis Features | Next: Interview Questions >>