Skip to main content

Caching Strategies

Caching is critical for microservices performance. This chapter covers distributed caching patterns and implementations.
Learning Objectives:
  • Implement various caching patterns
  • Design cache invalidation strategies
  • Use Redis for distributed caching
  • Handle cache failures gracefully
  • Optimize cache hit rates

Why Caching Matters

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CACHING IMPACT                                            │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  WITHOUT CACHING:                                                            │
│  ───────────────────                                                        │
│                                                                              │
│  Client ──▶ API Gateway ──▶ Order Service ──▶ Database                      │
│                                    │              │                          │
│                                    │              │ 50ms                     │
│                                    ├──▶ User Service ──▶ Database            │
│                                    │                        │ 30ms           │
│                                    └──▶ Product Service ──▶ Database         │
│                                                                │ 40ms        │
│                                                                              │
│  Total Latency: ~120ms (serial) or ~50ms (parallel)                         │
│  Database Load: Every request hits DB                                       │
│                                                                              │
│  ═══════════════════════════════════════════════════════════════════════════│
│                                                                              │
│  WITH CACHING:                                                               │
│  ─────────────                                                              │
│                                                                              │
│  Client ──▶ API Gateway ──▶ Order Service                                   │
│                                    │                                         │
│                                    ├──▶ Redis Cache ──▶ (hit) Return         │
│                                    │        │ 1ms                            │
│                                    │        └──▶ (miss) ──▶ Database         │
│                                    │                           │ 50ms        │
│                                                                              │
│  Cache Hit Latency: ~5ms                                                    │
│  Cache Miss Latency: ~55ms                                                  │
│  Database Load: Only cache misses hit DB                                    │
│                                                                              │
│  With 90% cache hit rate: Average latency = 0.9×5 + 0.1×55 = 10ms           │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Caching Patterns

Cache-Aside (Lazy Loading)

// Most common pattern - application manages cache

class ProductService {
  constructor(cache, repository) {
    this.cache = cache;  // Redis client
    this.repository = repository;  // Database
    this.ttl = 3600;  // 1 hour
  }

  async getProduct(productId) {
    const cacheKey = `product:${productId}`;
    
    // 1. Try to get from cache
    const cached = await this.cache.get(cacheKey);
    if (cached) {
      console.log(`Cache HIT for ${cacheKey}`);
      return JSON.parse(cached);
    }
    
    console.log(`Cache MISS for ${cacheKey}`);
    
    // 2. Get from database
    const product = await this.repository.findById(productId);
    if (!product) return null;
    
    // 3. Store in cache
    await this.cache.setEx(cacheKey, this.ttl, JSON.stringify(product));
    
    return product;
  }

  async updateProduct(productId, updates) {
    // Update database
    const product = await this.repository.update(productId, updates);
    
    // Invalidate cache
    await this.cache.del(`product:${productId}`);
    
    // Also invalidate any list caches
    await this.cache.del(`products:category:${product.categoryId}`);
    await this.cache.del('products:featured');
    
    return product;
  }
}

Write-Through

// Write to cache and database together

class UserService {
  constructor(cache, repository) {
    this.cache = cache;
    this.repository = repository;
    this.ttl = 7200;  // 2 hours
  }

  async createUser(userData) {
    // 1. Write to database
    const user = await this.repository.create(userData);
    
    // 2. Write to cache immediately
    const cacheKey = `user:${user.id}`;
    await this.cache.setEx(cacheKey, this.ttl, JSON.stringify(user));
    
    // Also cache by email for lookup
    await this.cache.setEx(`user:email:${user.email}`, this.ttl, user.id);
    
    return user;
  }

  async updateUser(userId, updates) {
    // Update database
    const user = await this.repository.update(userId, updates);
    
    // Update cache
    const cacheKey = `user:${userId}`;
    await this.cache.setEx(cacheKey, this.ttl, JSON.stringify(user));
    
    return user;
  }

  async getUser(userId) {
    const cacheKey = `user:${userId}`;
    
    // Try cache first
    const cached = await this.cache.get(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }
    
    // Fallback to database
    const user = await this.repository.findById(userId);
    if (user) {
      await this.cache.setEx(cacheKey, this.ttl, JSON.stringify(user));
    }
    
    return user;
  }
}

Write-Behind (Write-Back)

// Write to cache first, async write to database

class InventoryService {
  constructor(cache, repository, queue) {
    this.cache = cache;
    this.repository = repository;
    this.queue = queue;  // For async DB writes
  }

  async updateStock(productId, quantity) {
    const cacheKey = `inventory:${productId}`;
    
    // 1. Update cache immediately (fast response)
    const current = await this.cache.get(cacheKey);
    const currentStock = current ? parseInt(current) : 0;
    const newStock = currentStock + quantity;
    
    await this.cache.set(cacheKey, newStock.toString());
    
    // 2. Queue database write (async)
    await this.queue.add('inventory-update', {
      productId,
      quantity,
      newStock,
      timestamp: Date.now()
    });
    
    return { productId, stock: newStock };
  }

  async getStock(productId) {
    const cacheKey = `inventory:${productId}`;
    
    const stock = await this.cache.get(cacheKey);
    if (stock !== null) {
      return parseInt(stock);
    }
    
    // Load from DB if not in cache
    const inventory = await this.repository.findByProductId(productId);
    if (inventory) {
      await this.cache.set(cacheKey, inventory.stock.toString());
      return inventory.stock;
    }
    
    return 0;
  }
}

// Background worker processes the queue
class InventoryWriter {
  constructor(repository) {
    this.repository = repository;
  }

  async processUpdate(job) {
    const { productId, newStock } = job.data;
    
    // Batch writes for efficiency
    await this.repository.updateStock(productId, newStock);
  }
}

Read-Through

// Cache handles loading data

class ReadThroughCache {
  constructor(redisClient, dataLoader) {
    this.redis = redisClient;
    this.dataLoader = dataLoader;  // Function to load from DB
  }

  async get(key, options = {}) {
    const { ttl = 3600, loader } = options;
    
    // Try cache
    const cached = await this.redis.get(key);
    if (cached) {
      return JSON.parse(cached);
    }
    
    // Load using provided loader or default
    const loadFn = loader || this.dataLoader;
    const data = await loadFn(key);
    
    if (data !== null && data !== undefined) {
      await this.redis.setEx(key, ttl, JSON.stringify(data));
    }
    
    return data;
  }
}

// Usage
const cache = new ReadThroughCache(redis, async (key) => {
  // Default loader extracts ID from key
  const [type, id] = key.split(':');
  
  switch (type) {
    case 'user':
      return userRepository.findById(id);
    case 'product':
      return productRepository.findById(id);
    default:
      return null;
  }
});

// Get user (automatically loads if not cached)
const user = await cache.get('user:123');

Redis Implementation

Connection and Configuration

// cache/redis-client.js
const { createClient, createCluster } = require('redis');

class RedisCache {
  constructor(config = {}) {
    this.config = {
      url: process.env.REDIS_URL || 'redis://localhost:6379',
      cluster: process.env.REDIS_CLUSTER === 'true',
      ...config
    };
    
    this.client = null;
    this.isConnected = false;
  }

  async connect() {
    if (this.config.cluster) {
      // Redis Cluster for production
      this.client = createCluster({
        rootNodes: [
          { url: process.env.REDIS_NODE_1 },
          { url: process.env.REDIS_NODE_2 },
          { url: process.env.REDIS_NODE_3 }
        ],
        defaults: {
          socket: {
            connectTimeout: 5000,
            keepAlive: 5000
          }
        }
      });
    } else {
      // Single node for development
      this.client = createClient({
        url: this.config.url,
        socket: {
          connectTimeout: 5000,
          keepAlive: 5000,
          reconnectStrategy: (retries) => {
            if (retries > 10) {
              console.error('Redis: Max reconnection attempts reached');
              return new Error('Max reconnection attempts');
            }
            return Math.min(retries * 100, 3000);
          }
        }
      });
    }

    this.client.on('error', (err) => {
      console.error('Redis error:', err);
      this.isConnected = false;
    });

    this.client.on('connect', () => {
      console.log('Redis connected');
      this.isConnected = true;
    });

    this.client.on('reconnecting', () => {
      console.log('Redis reconnecting...');
    });

    await this.client.connect();
    return this;
  }

  async get(key) {
    try {
      return await this.client.get(key);
    } catch (error) {
      console.error(`Redis GET error for ${key}:`, error);
      return null;
    }
  }

  async set(key, value, options = {}) {
    try {
      const stringValue = typeof value === 'object' 
        ? JSON.stringify(value) 
        : value;
      
      if (options.ttl) {
        await this.client.setEx(key, options.ttl, stringValue);
      } else {
        await this.client.set(key, stringValue);
      }
      return true;
    } catch (error) {
      console.error(`Redis SET error for ${key}:`, error);
      return false;
    }
  }

  async setEx(key, seconds, value) {
    return this.set(key, value, { ttl: seconds });
  }

  async del(key) {
    try {
      await this.client.del(key);
      return true;
    } catch (error) {
      console.error(`Redis DEL error for ${key}:`, error);
      return false;
    }
  }

  async mget(keys) {
    try {
      return await this.client.mGet(keys);
    } catch (error) {
      console.error('Redis MGET error:', error);
      return keys.map(() => null);
    }
  }

  async mset(keyValues, ttl = null) {
    try {
      const pipeline = this.client.multi();
      
      for (const [key, value] of Object.entries(keyValues)) {
        const stringValue = typeof value === 'object' 
          ? JSON.stringify(value) 
          : value;
        
        if (ttl) {
          pipeline.setEx(key, ttl, stringValue);
        } else {
          pipeline.set(key, stringValue);
        }
      }
      
      await pipeline.exec();
      return true;
    } catch (error) {
      console.error('Redis MSET error:', error);
      return false;
    }
  }

  async disconnect() {
    if (this.client) {
      await this.client.quit();
    }
  }
}

module.exports = { RedisCache };

Caching Service

// cache/caching-service.js

class CachingService {
  constructor(redis) {
    this.redis = redis;
    this.defaultTTL = 3600;  // 1 hour
    this.prefix = process.env.SERVICE_NAME || 'app';
  }

  _key(key) {
    return `${this.prefix}:${key}`;
  }

  async get(key) {
    const data = await this.redis.get(this._key(key));
    return data ? JSON.parse(data) : null;
  }

  async set(key, value, ttl = this.defaultTTL) {
    return this.redis.setEx(this._key(key), ttl, JSON.stringify(value));
  }

  async getOrSet(key, fetchFn, ttl = this.defaultTTL) {
    // Try cache first
    let data = await this.get(key);
    
    if (data !== null) {
      return { data, cached: true };
    }
    
    // Fetch from source
    data = await fetchFn();
    
    if (data !== null && data !== undefined) {
      await this.set(key, data, ttl);
    }
    
    return { data, cached: false };
  }

  async invalidate(key) {
    return this.redis.del(this._key(key));
  }

  async invalidatePattern(pattern) {
    // Use SCAN to find matching keys (don't use KEYS in production)
    const keys = [];
    let cursor = 0;
    
    do {
      const result = await this.redis.client.scan(cursor, {
        MATCH: this._key(pattern),
        COUNT: 100
      });
      
      cursor = result.cursor;
      keys.push(...result.keys);
    } while (cursor !== 0);
    
    if (keys.length > 0) {
      await this.redis.client.del(keys);
    }
    
    return keys.length;
  }

  // Hash operations for objects
  async hget(key, field) {
    const data = await this.redis.client.hGet(this._key(key), field);
    return data ? JSON.parse(data) : null;
  }

  async hset(key, field, value, ttl = null) {
    await this.redis.client.hSet(this._key(key), field, JSON.stringify(value));
    
    if (ttl) {
      await this.redis.client.expire(this._key(key), ttl);
    }
  }

  async hgetall(key) {
    const data = await this.redis.client.hGetAll(this._key(key));
    
    const result = {};
    for (const [field, value] of Object.entries(data)) {
      result[field] = JSON.parse(value);
    }
    
    return result;
  }

  // Sorted sets for leaderboards, time-based data
  async zadd(key, score, member) {
    return this.redis.client.zAdd(this._key(key), { score, value: member });
  }

  async zrange(key, start, stop, options = {}) {
    return this.redis.client.zRange(this._key(key), start, stop, options);
  }

  // List operations for queues, recent items
  async lpush(key, ...values) {
    return this.redis.client.lPush(this._key(key), values);
  }

  async lrange(key, start, stop) {
    return this.redis.client.lRange(this._key(key), start, stop);
  }

  async ltrim(key, start, stop) {
    return this.redis.client.lTrim(this._key(key), start, stop);
  }
}

Cache Invalidation

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CACHE INVALIDATION STRATEGIES                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  1. TIME-BASED (TTL)                                                        │
│     ─────────────────                                                       │
│     • Set expiration time on cache entries                                  │
│     • Simple, but may serve stale data                                      │
│     • Good for: product catalogs, user profiles                             │
│                                                                              │
│  2. EVENT-BASED                                                             │
│     ────────────────                                                        │
│     • Invalidate on write events                                            │
│     • Real-time freshness                                                   │
│     • Good for: inventory, prices, orders                                   │
│                                                                              │
│  3. VERSION-BASED                                                           │
│     ─────────────────                                                       │
│     • Include version in cache key                                          │
│     • Change version to invalidate                                          │
│     • Good for: configuration, templates                                    │
│                                                                              │
│  4. TAG-BASED                                                               │
│     ─────────────────                                                       │
│     • Tag entries with categories                                           │
│     • Invalidate by tag                                                     │
│     • Good for: related data (all products in category)                     │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Event-Based Invalidation

// Event-driven cache invalidation across services

// Event consumer for cache invalidation
class CacheInvalidator {
  constructor(cache, eventBus) {
    this.cache = cache;
    this.eventBus = eventBus;
    
    this.setupListeners();
  }

  setupListeners() {
    // Product events
    this.eventBus.subscribe('product.updated', this.onProductUpdated.bind(this));
    this.eventBus.subscribe('product.deleted', this.onProductDeleted.bind(this));
    
    // Price events
    this.eventBus.subscribe('price.changed', this.onPriceChanged.bind(this));
    
    // Inventory events
    this.eventBus.subscribe('inventory.updated', this.onInventoryUpdated.bind(this));
    
    // Category events
    this.eventBus.subscribe('category.updated', this.onCategoryUpdated.bind(this));
  }

  async onProductUpdated(event) {
    const { productId, categoryId } = event;
    
    // Invalidate specific product
    await this.cache.invalidate(`product:${productId}`);
    
    // Invalidate product lists
    await this.cache.invalidatePattern(`products:list:*`);
    await this.cache.invalidate(`products:category:${categoryId}`);
    await this.cache.invalidate('products:featured');
    
    console.log(`Cache invalidated for product ${productId}`);
  }

  async onProductDeleted(event) {
    const { productId, categoryId } = event;
    
    await this.cache.invalidate(`product:${productId}`);
    await this.cache.invalidatePattern(`products:*`);
    
    console.log(`Cache invalidated for deleted product ${productId}`);
  }

  async onPriceChanged(event) {
    const { productId, oldPrice, newPrice } = event;
    
    // Invalidate product cache
    await this.cache.invalidate(`product:${productId}`);
    
    // Invalidate any price-based lists
    await this.cache.invalidate('products:deals');
    await this.cache.invalidatePattern('products:price-range:*');
    
    console.log(`Price cache invalidated for product ${productId}`);
  }

  async onInventoryUpdated(event) {
    const { productId, oldStock, newStock } = event;
    
    // Invalidate inventory cache
    await this.cache.invalidate(`inventory:${productId}`);
    
    // If went out of stock or back in stock
    if ((oldStock > 0 && newStock === 0) || (oldStock === 0 && newStock > 0)) {
      await this.cache.invalidatePattern(`products:*`);
    }
  }

  async onCategoryUpdated(event) {
    const { categoryId } = event;
    
    // Invalidate category and all products in it
    await this.cache.invalidate(`category:${categoryId}`);
    await this.cache.invalidate(`products:category:${categoryId}`);
    await this.cache.invalidatePattern('categories:*');
  }
}

Tag-Based Invalidation

// Tagging cache entries for group invalidation

class TaggedCache {
  constructor(redis) {
    this.redis = redis;
  }

  async set(key, value, options = {}) {
    const { ttl = 3600, tags = [] } = options;
    
    // Store the value
    await this.redis.setEx(key, ttl, JSON.stringify(value));
    
    // Store key in tag sets
    for (const tag of tags) {
      await this.redis.client.sAdd(`tag:${tag}`, key);
      await this.redis.client.expire(`tag:${tag}`, ttl * 2);  // Tags live longer
    }
    
    // Store tags for this key
    if (tags.length > 0) {
      await this.redis.client.sAdd(`key-tags:${key}`, tags);
      await this.redis.client.expire(`key-tags:${key}`, ttl);
    }
  }

  async get(key) {
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  async invalidateByTag(tag) {
    // Get all keys with this tag
    const keys = await this.redis.client.sMembers(`tag:${tag}`);
    
    if (keys.length === 0) return 0;
    
    // Delete all cached values
    await this.redis.client.del(keys);
    
    // Clean up tag sets
    for (const key of keys) {
      const keyTags = await this.redis.client.sMembers(`key-tags:${key}`);
      for (const t of keyTags) {
        await this.redis.client.sRem(`tag:${t}`, key);
      }
      await this.redis.client.del(`key-tags:${key}`);
    }
    
    // Delete the tag set
    await this.redis.client.del(`tag:${tag}`);
    
    return keys.length;
  }

  async invalidateByTags(tags) {
    let count = 0;
    for (const tag of tags) {
      count += await this.invalidateByTag(tag);
    }
    return count;
  }
}

// Usage
const cache = new TaggedCache(redis);

// Cache a product with tags
await cache.set(`product:123`, product, {
  ttl: 3600,
  tags: [`category:${product.categoryId}`, 'products', 'featured']
});

// Later: invalidate all products in a category
await cache.invalidateByTag('category:electronics');

// Or invalidate all featured products
await cache.invalidateByTag('featured');

Cache Patterns for Microservices

Multi-Level Caching

// L1: Local in-memory cache (fastest)
// L2: Distributed Redis cache (shared)
// L3: Database (source of truth)

const NodeCache = require('node-cache');

class MultiLevelCache {
  constructor(redis) {
    // L1: Local cache (per instance, fast but not shared)
    this.l1 = new NodeCache({
      stdTTL: 60,  // 1 minute
      checkperiod: 30,
      maxKeys: 1000
    });
    
    // L2: Redis (shared across instances)
    this.l2 = redis;
    
    this.l2TTL = 3600;  // 1 hour
  }

  async get(key, fetchFn) {
    // Try L1 (local memory)
    let data = this.l1.get(key);
    if (data !== undefined) {
      console.log(`L1 HIT: ${key}`);
      return data;
    }

    // Try L2 (Redis)
    const cached = await this.l2.get(key);
    if (cached) {
      console.log(`L2 HIT: ${key}`);
      data = JSON.parse(cached);
      
      // Populate L1
      this.l1.set(key, data);
      return data;
    }

    // L3: Fetch from source
    console.log(`CACHE MISS: ${key}`);
    data = await fetchFn();
    
    if (data !== null && data !== undefined) {
      // Populate both caches
      this.l1.set(key, data);
      await this.l2.setEx(key, this.l2TTL, JSON.stringify(data));
    }

    return data;
  }

  async invalidate(key) {
    // Invalidate both levels
    this.l1.del(key);
    await this.l2.del(key);
  }

  async invalidateLocal(key) {
    // Invalidate only local cache (for pub/sub invalidation)
    this.l1.del(key);
  }
}

// Cross-instance invalidation with Redis Pub/Sub
class CacheCoordinator {
  constructor(multiLevelCache, redis) {
    this.cache = multiLevelCache;
    this.redis = redis;
    this.channel = 'cache-invalidation';
    this.instanceId = process.env.INSTANCE_ID || require('crypto').randomUUID();
  }

  async setup() {
    // Subscribe to invalidation messages
    const subscriber = this.redis.duplicate();
    await subscriber.connect();
    
    await subscriber.subscribe(this.channel, (message) => {
      const { key, sourceInstance } = JSON.parse(message);
      
      // Don't process our own messages
      if (sourceInstance === this.instanceId) return;
      
      // Invalidate local cache
      this.cache.invalidateLocal(key);
      console.log(`Received invalidation for ${key} from ${sourceInstance}`);
    });
  }

  async invalidate(key) {
    // Invalidate locally and in Redis
    await this.cache.invalidate(key);
    
    // Notify other instances
    await this.redis.client.publish(this.channel, JSON.stringify({
      key,
      sourceInstance: this.instanceId
    }));
  }
}

Request Coalescing (Thundering Herd Prevention)

// Prevent multiple identical requests from hitting the database

class CoalescingCache {
  constructor(cache) {
    this.cache = cache;
    this.pending = new Map();  // In-flight requests
  }

  async get(key, fetchFn, ttl = 3600) {
    // Check cache first
    const cached = await this.cache.get(key);
    if (cached) {
      return JSON.parse(cached);
    }

    // Check if request is already in flight
    if (this.pending.has(key)) {
      console.log(`Coalescing request for ${key}`);
      return this.pending.get(key);
    }

    // Create new request
    const promise = this.fetchAndCache(key, fetchFn, ttl);
    this.pending.set(key, promise);

    try {
      const result = await promise;
      return result;
    } finally {
      this.pending.delete(key);
    }
  }

  async fetchAndCache(key, fetchFn, ttl) {
    const data = await fetchFn();
    
    if (data !== null && data !== undefined) {
      await this.cache.setEx(key, ttl, JSON.stringify(data));
    }
    
    return data;
  }
}

// Usage - even with 100 concurrent requests, only 1 DB query
const cache = new CoalescingCache(redis);

// Simulate 100 concurrent requests for same product
const requests = Array(100).fill().map(() =>
  cache.get('product:123', () => productRepository.findById('123'))
);

const results = await Promise.all(requests);
// Only 1 database query was made!

Cache Warming

// Pre-populate cache before traffic hits

class CacheWarmer {
  constructor(cache, repository) {
    this.cache = cache;
    this.repository = repository;
  }

  async warmAll() {
    console.log('Starting cache warming...');
    
    await Promise.all([
      this.warmFeaturedProducts(),
      this.warmCategories(),
      this.warmPopularProducts(),
      this.warmConfigurations()
    ]);
    
    console.log('Cache warming complete');
  }

  async warmFeaturedProducts() {
    const products = await this.repository.getFeaturedProducts();
    
    for (const product of products) {
      await this.cache.set(`product:${product.id}`, product, 3600);
    }
    
    await this.cache.set('products:featured', products, 3600);
    console.log(`Warmed ${products.length} featured products`);
  }

  async warmCategories() {
    const categories = await this.repository.getAllCategories();
    
    for (const category of categories) {
      await this.cache.set(`category:${category.id}`, category, 7200);
      
      // Also warm products per category
      const products = await this.repository.getProductsByCategory(category.id);
      await this.cache.set(`products:category:${category.id}`, products, 3600);
    }
    
    await this.cache.set('categories:all', categories, 7200);
    console.log(`Warmed ${categories.length} categories`);
  }

  async warmPopularProducts() {
    // Top 100 most viewed products
    const products = await this.repository.getPopularProducts(100);
    
    for (const product of products) {
      await this.cache.set(`product:${product.id}`, product, 3600);
    }
    
    console.log(`Warmed ${products.length} popular products`);
  }

  async warmConfigurations() {
    const configs = await this.repository.getAllConfigurations();
    
    for (const [key, value] of Object.entries(configs)) {
      await this.cache.set(`config:${key}`, value, 86400);  // 24 hours
    }
    
    console.log(`Warmed ${Object.keys(configs).length} configurations`);
  }
}

// Run on startup
app.on('ready', async () => {
  const warmer = new CacheWarmer(cache, repository);
  await warmer.warmAll();
});

// Also run periodically
setInterval(async () => {
  await warmer.warmPopularProducts();
}, 30 * 60 * 1000);  // Every 30 minutes

Cache Failure Handling

// Graceful degradation when cache fails

class ResilientCache {
  constructor(redis, options = {}) {
    this.redis = redis;
    this.fallbackTTL = options.fallbackTTL || 60;  // 1 minute local fallback
    this.localFallback = new Map();
    this.isHealthy = true;
    
    // Monitor health
    this.startHealthCheck();
  }

  startHealthCheck() {
    setInterval(async () => {
      try {
        await this.redis.client.ping();
        if (!this.isHealthy) {
          console.log('Redis connection restored');
          this.isHealthy = true;
          this.localFallback.clear();  // Clear stale fallback data
        }
      } catch (error) {
        if (this.isHealthy) {
          console.error('Redis connection lost:', error.message);
          this.isHealthy = false;
        }
      }
    }, 5000);  // Check every 5 seconds
  }

  async get(key) {
    if (!this.isHealthy) {
      // Use local fallback
      const fallback = this.localFallback.get(key);
      if (fallback && fallback.expires > Date.now()) {
        return fallback.value;
      }
      return null;
    }

    try {
      const data = await this.redis.get(key);
      
      if (data) {
        // Update local fallback
        this.localFallback.set(key, {
          value: data,
          expires: Date.now() + (this.fallbackTTL * 1000)
        });
      }
      
      return data;
    } catch (error) {
      console.error(`Cache get error for ${key}:`, error.message);
      this.isHealthy = false;
      
      // Try local fallback
      const fallback = this.localFallback.get(key);
      return fallback?.value || null;
    }
  }

  async set(key, value, ttl = 3600) {
    if (!this.isHealthy) {
      // Store in local fallback only
      this.localFallback.set(key, {
        value,
        expires: Date.now() + (Math.min(ttl, this.fallbackTTL) * 1000)
      });
      return false;
    }

    try {
      await this.redis.setEx(key, ttl, value);
      
      // Also update local fallback
      this.localFallback.set(key, {
        value,
        expires: Date.now() + (this.fallbackTTL * 1000)
      });
      
      return true;
    } catch (error) {
      console.error(`Cache set error for ${key}:`, error.message);
      this.isHealthy = false;
      
      // Store in local fallback
      this.localFallback.set(key, {
        value,
        expires: Date.now() + (this.fallbackTTL * 1000)
      });
      
      return false;
    }
  }

  getHealth() {
    return {
      healthy: this.isHealthy,
      fallbackSize: this.localFallback.size
    };
  }
}

Interview Questions

Answer:Cache-Aside (Lazy Loading):
  1. Application checks cache first
  2. On miss, fetch from database
  3. Store in cache for future requests
const data = await cache.get(key);
if (!data) {
  data = await db.fetch(key);
  await cache.set(key, data);
}
return data;
Pros:
  • Only cache what’s needed
  • Cache failures don’t break reads
  • Simple to implement
Cons:
  • First request is slow (cache miss)
  • Potential stale data
  • Three round trips on miss
Use for: Most read-heavy scenarios
Answer:Strategies:
  1. TTL-based: Set expiration, accept staleness
  2. Event-based: Publish events on writes, subscribers invalidate
  3. Tag-based: Group related entries, invalidate by tag
  4. Version-based: Include version in key, change on update
Best Practice for Microservices:
  • Publish events when data changes
  • Each service invalidates its own cache
  • Use short TTLs as safety net
// On update
await db.update(product);
await eventBus.publish('product.updated', { productId });

// Consumers invalidate their caches
eventBus.on('product.updated', (e) => cache.del(`product:${e.productId}`));
Answer:Problem: When cache expires, many concurrent requests hit database simultaneously.Solutions:
  1. Request Coalescing: Single request, others wait
if (pending.has(key)) return pending.get(key);
pending.set(key, fetchFromDB(key));
  1. Probabilistic Early Expiration: Randomly refresh before TTL
  2. Background Refresh: Refresh in background before expiry
  3. Locking: Only one process refreshes cache
if (await lock.acquire(key)) {
  data = await fetchFromDB();
  await cache.set(key, data);
  lock.release(key);
}
Answer:Write-Through:
  • Write to cache AND database in same operation
  • Strong consistency
  • Slower writes (two writes)
Use for: Data that must be consistent, user profilesWrite-Behind (Write-Back):
  • Write to cache first, async to database
  • Fast writes
  • Risk of data loss if cache fails
Use for: Analytics, metrics, high-frequency updatesDecision factors:
  • Consistency requirements
  • Write frequency
  • Tolerance for data loss
Answer:Challenges:
  • Each instance has local cache
  • Invalidation must reach all instances
  • Data inconsistency between instances
Solutions:
  1. Distributed Cache Only (Redis)
    • No local cache
    • Always consistent
    • Higher latency
  2. Multi-Level Cache + Pub/Sub
    • L1: Local (fast)
    • L2: Redis (shared)
    • Invalidate via pub/sub
  3. Short TTL for Local Cache
    • Local cache: 60s
    • Redis cache: 1 hour
    • Accept brief inconsistency
// Pub/sub invalidation
redis.subscribe('cache-invalidate', (key) => {
  localCache.del(key);
});

Chapter Summary

Key Takeaways:
  • Choose the right caching pattern (cache-aside, write-through, write-behind)
  • Implement robust invalidation strategies
  • Handle cache failures gracefully with fallbacks
  • Use multi-level caching for performance
  • Prevent thundering herd with request coalescing
  • Warm caches proactively for predictable latency
Next Chapter: Load Balancing - Distribution strategies for microservices.