Redis Caching — Cache-Aside Pattern
Caching reduces database load and response latency by storing frequently accessed data in memory (Redis). The cache-aside (lazy loading) pattern is the most common: check cache first, on miss fetch from DB and populate cache, on write invalidate the relevant cache keys.
Cache Service Implementation
import Redis from 'ioredis';
import { prisma } from './prismaClient';
const redis = new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379', {
maxRetriesPerRequest: 3,
lazyConnect: true,
enableReadyCheck: true,
});
// ━━ Generic cache wrapper ━━
class CacheService {
constructor(private readonly redis: Redis, private readonly defaultTTL = 3600) {}
async get<T>(key: string): Promise<T | null> {
const data = await this.redis.get(key);
if (!data) return null;
return JSON.parse(data) as T;
}
async set(key: string, value: unknown, ttl = this.defaultTTL): Promise<void> {
await this.redis.setex(key, ttl, JSON.stringify(value));
}
async del(...keys: string[]): Promise<void> {
if (keys.length > 0) await this.redis.del(...keys);
}
async delPattern(pattern: string): Promise<void> {
// Delete all keys matching a pattern e.g. 'products:*'
const keys = await this.redis.keys(pattern);
if (keys.length > 0) await this.redis.del(...keys);
}
// Tag-based invalidation: group related keys under a tag
async invalidateTag(tag: string): Promise<void> {
const tagKey = `tag:${tag}`;
const keys = await this.redis.smembers(tagKey);
if (keys.length > 0) await this.redis.del(...keys);
await this.redis.del(tagKey);
}
async setWithTag(key: string, value: unknown, tag: string, ttl = this.defaultTTL): Promise<void> {
const pipeline = this.redis.pipeline();
pipeline.setex(key, ttl, JSON.stringify(value));
pipeline.sadd(`tag:${tag}`, key); // register key under tag
pipeline.expire(`tag:${tag}`, ttl); // tag set also expires
await pipeline.exec();
}
}
export const cache = new CacheService(redis);
// ━━ Cache-aside pattern in ProductService ━━
export class ProductService {
private readonly CACHE_TTL = 300; // 5 minutes
async getProduct(id: string) {
const cacheKey = `product:${id}`;
// 1. Check cache
const cached = await cache.get<Product>(cacheKey);
if (cached) return cached; // cache hit — return immediately
// 2. Cache miss — fetch from database
const product = await prisma.product.findUnique({
where: { id },
include: { category: true },
});
if (!product) return null;
// 3. Populate cache
await cache.set(cacheKey, product, this.CACHE_TTL);
return product;
}
async updateProduct(id: string, data: Partial<Product>): Promise<Product> {
const updated = await prisma.product.update({ where: { id }, data });
// Invalidate: this product AND the list (might show stale data)
await cache.del(`product:${id}`);
await cache.delPattern('products:list:*'); // all paginated list caches
return updated;
}
async listProducts(page: number, limit: number) {
const cacheKey = `products:list:${page}:${limit}`;
const cached = await cache.get(cacheKey);
if (cached) return cached;
const [products, total] = await Promise.all([
prisma.product.findMany({ skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' } }),
prisma.product.count(),
]);
const result = { products, total, page, totalPages: Math.ceil(total / limit) };
await cache.set(cacheKey, result, 60); // 1 minute for list pages
return result;
}
}
interface Product { id: string; name: string; price: number; }Tip
Tip
Practice Redis Caching CacheAside Pattern in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Cache invalidation is the hardest problem. Always set TTL.
Practice Task
Note
Practice Task — (1) Write a working example of Redis Caching CacheAside Pattern from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Redis Caching CacheAside Pattern is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready node code.