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

3.16 — Quick Revision: Caching with Redis

A concise cheat sheet covering Redis data types, essential commands, caching patterns, Pub/Sub, TTL management, and common integration patterns for rapid review.


<< Previous: Interview Questions | Next Section: 3.17 — Error Handling >>


Caching Patterns

CACHE-ASIDE (Lazy Loading)
  1. Check cache        → HIT? Return cached data
  2. Cache MISS         → Query database
  3. Store in cache     → SET key value EX ttl
  4. Return data

WRITE-THROUGH
  1. Write to DB        → Update database
  2. Write to cache     → SET updated data in cache
  (Cache always in sync, but writes are slower)

WRITE-BEHIND
  1. Write to cache     → Fast, return immediately
  2. Async write to DB  → Background job writes to database later
  (Fast writes, risk of data loss)

Redis Data Types

TypeCommandsUse CaseExample
StringSET, GET, INCR, MSET, MGETCaching, counters, locksSET user:name "Alice" EX 3600
HashHSET, HGET, HGETALL, HINCRBYObjects, profilesHSET user:1 name "Alice" age "30"
ListLPUSH, RPUSH, LPOP, RPOP, LRANGEQueues, feedsLPUSH queue:jobs "job1"
SetSADD, SMEMBERS, SINTER, SUNIONUnique tracking, tagsSADD online-users "user:1"
Sorted SetZADD, ZRANGE, ZREVRANGE, ZRANKLeaderboards, rankingsZADD scores 100 "Alice"

Essential Redis Commands

KEY MANAGEMENT
  SET key value [EX sec] [NX]   Store a value (with optional TTL, only-if-new)
  GET key                       Retrieve a value
  DEL key [key ...]             Delete key(s)
  EXISTS key                    Check if key exists (1/0)
  TYPE key                      Get data type of key
  RENAME old new                Rename a key

TTL MANAGEMENT
  EXPIRE key seconds            Set TTL on existing key
  PEXPIRE key milliseconds      Set TTL in milliseconds
  TTL key                       Get remaining TTL (-1=no expiry, -2=not found)
  PTTL key                      Get remaining TTL in milliseconds
  PERSIST key                   Remove TTL (make permanent)

STRINGS
  SET key value                 Store string
  GET key                       Retrieve string
  INCR key / DECR key           Atomic increment/decrement by 1
  INCRBY key n / DECRBY key n   Atomic increment/decrement by n
  MSET k1 v1 k2 v2             Set multiple keys
  MGET k1 k2                   Get multiple keys
  SETNX key value               Set only if NOT exists

HASHES
  HSET key field value [f v]    Set field(s) in hash
  HGET key field                Get single field
  HGETALL key                   Get all fields and values
  HDEL key field                Delete a field
  HEXISTS key field             Check field exists
  HINCRBY key field n           Increment numeric field
  HKEYS key / HVALS key         Get all field names / values
  HLEN key                      Count fields

LISTS
  LPUSH key val [val ...]       Push to head
  RPUSH key val [val ...]       Push to tail
  LPOP key / RPOP key           Pop from head / tail
  LRANGE key start stop         Get range (0 -1 = all)
  LLEN key                      Get length
  LTRIM key start stop          Keep only range, remove rest
  BLPOP key timeout             Blocking pop (for queues)

SETS
  SADD key member [member ...]  Add member(s)
  SMEMBERS key                  Get all members
  SISMEMBER key member          Check membership (1/0)
  SCARD key                     Count members
  SREM key member               Remove member
  SINTER key1 key2              Intersection
  SUNION key1 key2              Union
  SDIFF key1 key2               Difference

SORTED SETS
  ZADD key score member [...]   Add with score
  ZRANGE key start stop [REV]   Get by rank (ascending/descending)
  ZRANGEBYSCORE key min max     Get by score range
  ZSCORE key member             Get member's score
  ZRANK key member              Get rank (ascending, 0-based)
  ZREVRANK key member           Get rank (descending, 0-based)
  ZINCRBY key n member          Increment score
  ZCARD key                     Count members
  ZREM key member               Remove member

SERVER
  PING                          Test connection (returns PONG)
  INFO [section]                Server info and stats
  DBSIZE                        Number of keys
  FLUSHDB                       Delete all keys in current DB
  SCAN cursor [MATCH pat]       Iterate keys (production-safe)
  MONITOR                       Watch all commands in real time

node-redis v4+ Cheat Sheet

// SETUP
const { createClient } = require('redis');
const client = createClient({ url: 'redis://localhost:6379' });
client.on('error', (err) => console.error('Redis Error:', err));
await client.connect();

// STRINGS
await client.set('key', 'value', { EX: 3600 });  // SET with TTL
const val = await client.get('key');               // GET
await client.del('key');                           // DEL
await client.incr('counter');                      // INCR
await client.incrBy('counter', 10);               // INCRBY

// HASHES
await client.hSet('user:1', { name: 'Alice', age: '30' });
const name = await client.hGet('user:1', 'name');
const all = await client.hGetAll('user:1');
await client.hIncrBy('user:1', 'views', 1);

// LISTS
await client.lPush('queue', 'item');
await client.rPush('queue', 'item');
const items = await client.lRange('queue', 0, -1);
const popped = await client.lPop('queue');
await client.lTrim('feed', 0, 99);  // Keep first 100

// SETS
await client.sAdd('tags', ['js', 'redis']);
const members = await client.sMembers('tags');
const isMember = await client.sIsMember('tags', 'js');
const common = await client.sInter(['set1', 'set2']);

// SORTED SETS
await client.zAdd('board', [{ score: 100, value: 'Alice' }]);
const top = await client.zRange('board', 0, 9, { REV: true });
const rank = await client.zRevRank('board', 'Alice');
await client.zIncrBy('board', 10, 'Alice');

// TTL
await client.expire('key', 3600);
const ttl = await client.ttl('key');

// TRANSACTIONS
const results = await client.multi()
  .set('a', '1')
  .set('b', '2')
  .incr('a')
  .exec();

// PUB/SUB (use separate client for subscriber!)
await publisher.publish('channel', JSON.stringify(data));
await subscriber.subscribe('channel', (msg) => { /* ... */ });

// CLEANUP
await client.quit();

Caching Middleware Pattern

function cache(prefix, ttl = 300) {
  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 orig = res.json.bind(res);
      res.json = (body) => {
        if (res.statusCode < 300)
          redis.set(key, JSON.stringify(body), { EX: ttl }).catch(() => {});
        return orig(body);
      };
      next();
    } catch { next(); }  // Fail open
  };
}

Cache Invalidation Patterns

TTL-BASED
  SET key value EX 3600          → Auto-expires in 1 hour
  (Safety net, always use)

EVENT-BASED
  On CREATE → DEL list caches   → SCAN + DEL "products:*"
  On UPDATE → DEL specific + list caches
  On DELETE → DEL specific + list caches

SCAN (production-safe pattern matching)
  let cursor = 0;
  do {
    result = await client.scan(cursor, { MATCH: 'products:*', COUNT: 100 });
    cursor = result.cursor;
    if (result.keys.length) await client.del(result.keys);
  } while (cursor !== 0);

Pub/Sub Pattern

PUBLISHER                        SUBSCRIBER
  publish(channel, msg) ───────> subscribe(channel, callback)
                                 pSubscribe('events:*', callback)

  - Fire-and-forget              - Must use SEPARATE Redis client
  - No persistence               - Cannot run other commands
  - All subscribers receive       - Pattern matching with pSubscribe

Rate Limiting Pattern

FIXED WINDOW
  Key: ratelimit:{ip}:{minute}
  INCR key → count
  If count == 1: EXPIRE key 60
  If count > limit: 429 Too Many Requests

SLIDING WINDOW (Sorted Set)
  ZREMRANGEBYSCORE key -inf (now - window)
  ZADD key now unique_id
  ZCARD key → count
  EXPIRE key window
  If count > limit: 429

Session Store (connect-redis)

const RedisStore = require('connect-redis').default;
app.use(session({
  store: new RedisStore({ client: redisClient, prefix: 'sess:', ttl: 86400 }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true, maxAge: 86400000 },
}));

TTL Guidelines

Data TypeTTLReasoning
User session24hSecurity vs convenience
Product catalog1-6hInfrequent changes
Search results5-15minModerate changes
Rate limit counters1minMust be fresh
Static config24hRarely changes
Real-time data5-30sFrequent changes

Persistence Comparison

FeatureRDB (Snapshots)AOF (Append Log)
Data lossBetween snapshots~1 second (everysec)
File sizeCompactLarger
Restart speedFastSlower
I/O overheadLowHigher
ProductionUse for backupsUse for durability
CombinedRecommended: RDB + AOF together

Common Pitfalls

PitfallSolution
Using KEYS * in productionUse SCAN with cursor
No TTL on cache entriesAlways set TTL as safety net
Caching errors/404sOnly cache successful responses
Same cache key for different usersInclude user ID in key for personalized data
Redis failure breaks the appWrap in try/catch, fail open to database
Storing objects without stringifyAlways JSON.stringify() / JSON.parse()
Using one client for Pub/Sub + commandsUse separate clients

<< Previous: Interview Questions | Next Section: 3.17 — Error Handling >>