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
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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)
Copy
// 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
Copy
// 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)
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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
Copy
// 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
Copy
// 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
Copy
// 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)
Copy
// 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
Copy
// 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
Copy
// 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
Q1: What is cache-aside pattern and when to use it?
Q1: What is cache-aside pattern and when to use it?
Answer:Cache-Aside (Lazy Loading):Pros:
- Application checks cache first
- On miss, fetch from database
- Store in cache for future requests
Copy
const data = await cache.get(key);
if (!data) {
data = await db.fetch(key);
await cache.set(key, data);
}
return data;
- Only cache what’s needed
- Cache failures don’t break reads
- Simple to implement
- First request is slow (cache miss)
- Potential stale data
- Three round trips on miss
Q2: How do you handle cache invalidation in microservices?
Q2: How do you handle cache invalidation in microservices?
Answer:Strategies:
- TTL-based: Set expiration, accept staleness
- Event-based: Publish events on writes, subscribers invalidate
- Tag-based: Group related entries, invalidate by tag
- Version-based: Include version in key, change on update
- Publish events when data changes
- Each service invalidates its own cache
- Use short TTLs as safety net
Copy
// 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}`));
Q3: What is the thundering herd problem and how to prevent it?
Q3: What is the thundering herd problem and how to prevent it?
Answer:Problem: When cache expires, many concurrent requests hit database simultaneously.Solutions:
- Request Coalescing: Single request, others wait
Copy
if (pending.has(key)) return pending.get(key);
pending.set(key, fetchFromDB(key));
- Probabilistic Early Expiration: Randomly refresh before TTL
- Background Refresh: Refresh in background before expiry
- Locking: Only one process refreshes cache
Copy
if (await lock.acquire(key)) {
data = await fetchFromDB();
await cache.set(key, data);
lock.release(key);
}
Q4: When would you use write-through vs write-behind caching?
Q4: When would you use write-through vs write-behind caching?
Answer:Write-Through:
- Write to cache AND database in same operation
- Strong consistency
- Slower writes (two writes)
- Write to cache first, async to database
- Fast writes
- Risk of data loss if cache fails
- Consistency requirements
- Write frequency
- Tolerance for data loss
Q5: How do you handle cache in a multi-instance deployment?
Q5: How do you handle cache in a multi-instance deployment?
Answer:Challenges:
- Each instance has local cache
- Invalidation must reach all instances
- Data inconsistency between instances
- Distributed Cache Only (Redis)
- No local cache
- Always consistent
- Higher latency
- Multi-Level Cache + Pub/Sub
- L1: Local (fast)
- L2: Redis (shared)
- Invalidate via pub/sub
- Short TTL for Local Cache
- Local cache: 60s
- Redis cache: 1 hour
- Accept brief inconsistency
Copy
// 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