Caching
Chapter 33: Caching Strategies
Section titled “Chapter 33: Caching Strategies”Implementing Effective Caching with TypeORM
Section titled “Implementing Effective Caching with TypeORM”33.1 Caching Overview
Section titled “33.1 Caching Overview”Caching stores frequently accessed data in fast-access memory to reduce database load and improve response times.
Caching Layers ================================================================================
Request Flow:
+------------------+ +------------------+ +------------------+ | Client | --> | Application | --> | Database | +------------------+ +------------------+ +------------------+ | v +------------------+ | Cache | | (Redis/Memory) | +------------------+
Cache Hit: Return from cache (fast) Cache Miss: Query database, store in cache, return
Benefits: - Reduced database load - Faster response times - Lower latency - Better scalability
================================================================================33.2 TypeORM Query Cache
Section titled “33.2 TypeORM Query Cache”Enable Query Caching
Section titled “Enable Query Caching”import { DataSource } from 'typeorm';
export default new DataSource({ type: 'postgres', // ... connection settings
// Enable query caching cache: true, // Simple enable with defaults
// Or with configuration cache: { type: 'redis', // 'redis', 'ioredis', or 'database' options: { host: 'localhost', port: 6379, password: 'secret', db: 0, }, duration: 30000, // Default TTL: 30 seconds ignoreErrors: true, // Don't throw on cache errors },});Using Query Cache
Section titled “Using Query Cache”// Cache query resultconst users = await userRepository .createQueryBuilder('user') .cache(true) // Use default TTL .getMany();
// Cache with custom duration (milliseconds)const users = await userRepository .createQueryBuilder('user') .cache(60000) // 1 minute .getMany();
// Cache with custom keyconst users = await userRepository .createQueryBuilder('user') .cache('users_active_list', 60000) .getMany();
// Cache with query identifierconst users = await userRepository .createQueryBuilder('user') .where('user.isActive = :active', { active: true }) .cache('active_users', 120000) // 2 minutes .getMany();Cache Management
Section titled “Cache Management”import { Injectable, InjectDataSource } from '@nestjs/common';import { DataSource } from 'typeorm';
@Injectable()export class CacheService { constructor( @InjectDataSource() private dataSource: DataSource, ) {}
// Clear all cache async clearAll(): Promise<void> { await this.dataSource.queryResultCache?.clear(); }
// Remove specific cache key async remove(key: string): Promise<void> { await this.dataSource.queryResultCache?.remove([key]); }
// Remove multiple keys async removeMany(keys: string[]): Promise<void> { await this.dataSource.queryResultCache?.remove(keys); }}33.3 NestJS Cache Manager
Section titled “33.3 NestJS Cache Manager”Setup Cache Module
Section titled “Setup Cache Module”import { Module } from '@nestjs/common';import { CacheModule, CacheStore } from '@nestjs/cache-manager';import { TypeOrmModule } from '@nestjs/typeorm';import * as redisStore from 'cache-manager-redis-yet';
@Module({ imports: [ CacheModule.registerAsync({ isGlobal: true, useFactory: async () => { const store = await redisStore({ socket: { host: 'localhost', port: 6379, }, ttl: 30000, // Default TTL });
return { store: store as unknown as CacheStore, }; }, }), TypeOrmModule.forRoot({ // ... database config }), ],})export class AppModule {}Using Cache in Services
Section titled “Using Cache in Services”import { Injectable, Inject } from '@nestjs/common';import { CACHE_MANAGER } from '@nestjs/cache-manager';import { Cache } from 'cache-manager';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { User } from './user.entity';
@Injectable()export class UsersService { constructor( @InjectRepository(User) private userRepository: Repository<User>, @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {}
async findOne(id: number): Promise<User> { // Try cache first const cacheKey = `user:${id}`; const cached = await this.cacheManager.get<User>(cacheKey);
if (cached) { return cached; }
// Cache miss - query database const user = await this.userRepository.findOne({ where: { id }, });
if (user) { // Store in cache await this.cacheManager.set(cacheKey, user, 60000); }
return user; }
async update(id: number, data: Partial<User>): Promise<User> { const user = await this.userRepository.save({ id, ...data });
// Invalidate cache await this.cacheManager.del(`user:${id}`);
return user; }
async remove(id: number): Promise<void> { await this.userRepository.delete(id);
// Invalidate cache await this.cacheManager.del(`user:${id}`); }}33.4 Cache-Aside Pattern
Section titled “33.4 Cache-Aside Pattern”Implementation
Section titled “Implementation”import { Injectable, Inject } from '@nestjs/common';import { CACHE_MANAGER } from '@nestjs/cache-manager';import { Cache } from 'cache-manager';
@Injectable()export class CacheAsideService { constructor( @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {}
/** * Get from cache or execute factory function */ async getOrSet<T>( key: string, factory: () => Promise<T>, ttl: number = 30000, ): Promise<T> { // Try cache const cached = await this.cacheManager.get<T>(key);
if (cached !== undefined && cached !== null) { return cached; }
// Execute factory const value = await factory();
// Store in cache await this.cacheManager.set(key, value, ttl);
return value; }
/** * Get from cache or execute factory with null handling */ async getOrSetWithNull<T>( key: string, factory: () => Promise<T | null>, ttl: number = 30000, ): Promise<T | null> { const cached = await this.cacheManager.get<T | null>(key);
// Check if we have a cached value (including explicit null) if (cached !== undefined) { return cached; }
const value = await factory();
// Cache even null values to prevent cache stampede await this.cacheManager.set(key, value ?? null, ttl);
return value; }
/** * Invalidate multiple keys by pattern */ async invalidatePattern(pattern: string): Promise<void> { // For Redis-based cache const store = this.cacheManager.store as any;
if (store.keys) { const keys = await store.keys(pattern); await Promise.all(keys.map((key: string) => this.cacheManager.del(key))); } }}Usage Example
Section titled “Usage Example”import { Injectable } from '@nestjs/common';import { CacheAsideService } from '../common/cache/cache-aside.service';import { InjectRepository } from '@nestjs/typeorm';import { Repository } from 'typeorm';import { User } from './user.entity';
@Injectable()export class UsersService { constructor( @InjectRepository(User) private userRepository: Repository<User>, private cacheAside: CacheAsideService, ) {}
async findOne(id: number): Promise<User | null> { return this.cacheAside.getOrSetWithNull( `user:${id}`, () => this.userRepository.findOne({ where: { id } }), 60000, // 1 minute TTL ); }
async findByEmail(email: string): Promise<User | null> { return this.cacheAside.getOrSetWithNull( `user:email:${email}`, () => this.userRepository.findOne({ where: { email } }), 300000, // 5 minutes TTL ); }
async update(id: number, data: Partial<User>): Promise<User> { const user = await this.userRepository.save({ id, ...data });
// Invalidate related caches await this.cacheAside.invalidatePattern(`user:*${id}*`); await this.cacheAside.invalidatePattern(`user:email:*`);
return user; }}33.5 Caching Strategies
Section titled “33.5 Caching Strategies”Cache Invalidation Strategies
Section titled “Cache Invalidation Strategies” Cache Invalidation Strategies ================================================================================
1. Write-Through - Update cache immediately when data changes - Pros: Cache always consistent - Cons: Higher write latency
2. Write-Behind (Write-Back) - Update cache, async update database - Pros: Fast writes - Cons: Risk of data loss
3. Write-Around - Update database, invalidate cache - Pros: No cache write overhead - Cons: Cache miss on next read
4. Read-Through - Cache loads data on miss - Pros: Simplified application code - Cons: Initial latency on miss
Recommended: Write-Around + Read-Through (Cache-Aside)
================================================================================Time-Based Invalidation
Section titled “Time-Based Invalidation”// Different TTL for different data typesconst TTL = { USER_PROFILE: 300000, // 5 minutes USER_LIST: 60000, // 1 minute CONFIGURATION: 3600000, // 1 hour STATIC_DATA: 86400000, // 24 hours SESSION: 1800000, // 30 minutes};
// Usageawait this.cacheAside.getOrSet( `user:${id}`, () => this.userRepository.findOne({ where: { id } }), TTL.USER_PROFILE,);Event-Based Invalidation
Section titled “Event-Based Invalidation”import { Injectable } from '@nestjs/common';import { EventEmitter2 } from '@nestjs/event-emitter';import { CacheAsideService } from '../common/cache/cache-aside.service';
@Injectable()export class UsersService { constructor( private userRepository: Repository<User>, private cacheAside: CacheAsideService, private eventEmitter: EventEmitter2, ) {}
async update(id: number, data: Partial<User>): Promise<User> { const user = await this.userRepository.save({ id, ...data });
// Emit event for cache invalidation this.eventEmitter.emit('user.updated', { userId: id });
return user; }}
// src/users/listeners/cache.listener.tsimport { Injectable, OnModuleInit } from '@nestjs/common';import { OnEvent } from '@nestjs/event-emitter';import { CacheAsideService } from '../../common/cache/cache-aside.service';
@Injectable()export class CacheListener { constructor(private cacheAside: CacheAsideService) {}
@OnEvent('user.updated') async handleUserUpdated(event: { userId: number }) { await this.cacheAside.invalidatePattern(`user:*${event.userId}*`); }
@OnEvent('user.deleted') async handleUserDeleted(event: { userId: number }) { await this.cacheAside.invalidatePattern(`user:*${event.userId}*`); }}33.6 Advanced Caching Patterns
Section titled “33.6 Advanced Caching Patterns”Cache Stampede Prevention
Section titled “Cache Stampede Prevention”import { Injectable, Inject } from '@nestjs/common';import { CACHE_MANAGER } from '@nestjs/cache-manager';import { Cache } from 'cache-manager';
@Injectable()export class StampedeProofCacheService { private pendingRequests = new Map<string, Promise<any>>();
constructor( @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {}
async getOrSet<T>( key: string, factory: () => Promise<T>, ttl: number = 30000, ): Promise<T> { // Check cache const cached = await this.cacheManager.get<T>(key); if (cached !== undefined) { return cached; }
// Check if request is already pending const pending = this.pendingRequests.get(key); if (pending) { return pending; }
// Create new request const request = this.executeFactory(key, factory, ttl); this.pendingRequests.set(key, request);
try { return await request; } finally { this.pendingRequests.delete(key); } }
private async executeFactory<T>( key: string, factory: () => Promise<T>, ttl: number, ): Promise<T> { const value = await factory(); await this.cacheManager.set(key, value, ttl); return value; }}Multi-Level Caching
Section titled “Multi-Level Caching”import { Injectable } from '@nestjs/common';
interface CacheLayer { get<T>(key: string): Promise<T | undefined>; set<T>(key: string, value: T, ttl: number): Promise<void>; del(key: string): Promise<void>;}
@Injectable()export class MultiLevelCacheService { private layers: CacheLayer[] = [];
addLayer(layer: CacheLayer): void { this.layers.push(layer); }
async get<T>(key: string): Promise<T | undefined> { for (const layer of this.layers) { const value = await layer.get<T>(key); if (value !== undefined) { // Populate upper layers await this.populateUpperLayers(key, value, layer); return value; } } return undefined; }
async set<T>(key: string, value: T, ttl: number): Promise<void> { await Promise.all( this.layers.map(layer => layer.set(key, value, ttl)) ); }
async del(key: string): Promise<void> { await Promise.all( this.layers.map(layer => layer.del(key)) ); }
private async populateUpperLayers<T>( key: string, value: T, foundLayer: CacheLayer, ): Promise<void> { const index = this.layers.indexOf(foundLayer); const upperLayers = this.layers.slice(0, index);
await Promise.all( upperLayers.map(layer => layer.set(key, value, 30000)) ); }}33.7 Cache Decorators
Section titled “33.7 Cache Decorators”Custom Cache Decorator
Section titled “Custom Cache Decorator”import { SetMetadata } from '@nestjs/common';
export const CACHE_KEY = 'cache:key';export const CACHE_TTL = 'cache:ttl';
export const Cacheable = (key: string, ttl: number = 30000) => { return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { SetMetadata(CACHE_KEY, key)(target, propertyKey, descriptor); SetMetadata(CACHE_TTL, ttl)(target, propertyKey, descriptor); };};
// src/common/interceptors/cache.interceptor.tsimport { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject,} from '@nestjs/common';import { Reflector } from '@nestjs/core';import { CACHE_MANAGER } from '@nestjs/cache-manager';import { Cache } from 'cache-manager';import { Observable, from, of } from 'rxjs';import { switchMap, tap } from 'rxjs/operators';import { CACHE_KEY, CACHE_TTL } from '../decorators/cacheable.decorator';
@Injectable()export class CacheInterceptor implements NestInterceptor { constructor( private reflector: Reflector, @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const cacheKey = this.reflector.get<string>(CACHE_KEY, context.getHandler()); const cacheTtl = this.reflector.get<number>(CACHE_TTL, context.getHandler());
if (!cacheKey) { return next.handle(); }
return from(this.cacheManager.get(cacheKey)).pipe( switchMap(cached => { if (cached !== undefined) { return of(cached); }
return next.handle().pipe( tap(data => { this.cacheManager.set(cacheKey, data, cacheTtl || 30000); }), ); }), ); }}
// Usage@Injectable()export class UsersService { @Cacheable('users:all', 60000) async findAll(): Promise<User[]> { return this.userRepository.find(); }
@Cacheable('user:${id}', 300000) // Note: key interpolation not supported async findOne(id: number): Promise<User> { return this.userRepository.findOne({ where: { id } }); }}33.8 Cache Monitoring
Section titled “33.8 Cache Monitoring”Cache Statistics Service
Section titled “Cache Statistics Service”import { Injectable, Inject } from '@nestjs/common';import { CACHE_MANAGER } from '@nestjs/cache-manager';import { Cache } from 'cache-manager';
interface CacheStats { hits: number; misses: number; hitRate: number;}
@Injectable()export class CacheStatsService { private stats = new Map<string, CacheStats>();
constructor( @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {}
async getWithStats<T>( key: string, factory: () => Promise<T>, ttl: number = 30000, ): Promise<T> { const cached = await this.cacheManager.get<T>(key);
if (cached !== undefined) { this.recordHit(key); return cached; }
this.recordMiss(key); const value = await factory(); await this.cacheManager.set(key, value, ttl);
return value; }
private recordHit(key: string): void { const stats = this.getOrCreateStats(key); stats.hits++; stats.hitRate = stats.hits / (stats.hits + stats.misses); }
private recordMiss(key: string): void { const stats = this.getOrCreateStats(key); stats.misses++; stats.hitRate = stats.hits / (stats.hits + stats.misses); }
private getOrCreateStats(key: string): CacheStats { if (!this.stats.has(key)) { this.stats.set(key, { hits: 0, misses: 0, hitRate: 0 }); } return this.stats.get(key)!; }
getStats(): Map<string, CacheStats> { return this.stats; }
getOverallStats(): CacheStats { let totalHits = 0; let totalMisses = 0;
this.stats.forEach(stat => { totalHits += stat.hits; totalMisses += stat.misses; });
return { hits: totalHits, misses: totalMisses, hitRate: totalHits / (totalHits + totalMisses) || 0, }; }}33.9 Summary
Section titled “33.9 Summary” Caching Quick Reference +------------------------------------------------------------------+ | | | Strategy | Use Case | | -------------------|------------------------------------------| | Cache-Aside | General purpose, most flexible | | Write-Through | Strong consistency needed | | Write-Around | Reduce cache writes | | Read-Through | Simplified application code | | | | TTL Guidelines | Data Type | | -------------------|------------------------------------------| | 30 seconds | Frequently changing data | | 1-5 minutes | User profiles, lists | | 1 hour | Configuration, settings | | 24 hours | Static data, reference tables | | | | Best Practices | Description | | -------------------|------------------------------------------| | Cache keys | Use consistent naming convention | | Invalidation | Invalidate on data change | | Stampede | Prevent concurrent cache misses | | Monitoring | Track hit rates and performance | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 34: Connection Pooling
Last Updated: February 2026