Caching Strategies

+15 Mana ✨

Introduction

Caching stores computed results for fast retrieval. The right caching strategy dramatically reduces database load and response times. Understanding cache invalidation, TTLs, and cache levels is essential for performance.

Key Concepts

  • Cache Aside: Application manages cache explicitly
  • Write Through: Write to cache and database simultaneously
  • Cache Invalidation: Removing stale cached data
  • TTL (Time To Live): Automatic cache expiration

Real World Context

Caching reduces:

  • Database query load
  • API response times
  • External service calls
  • Computed result recalculation

Deep Dive

NestJS 11: Keyv Integration

NestJS 11's CacheModule uses Keyv under the hood. The cache-manager package has migrated to Keyv for a unified key-value storage interface. This provides a cleaner API and better multi-store support.

Breaking change in NestJS 11: Cached data is now stored as {"value": "yourData", "expires": 1678901234567} internally. If you're migrating from NestJS 10, previously cached data will be incompatible. Flush your cache stores after upgrading.

For basic in-memory caching, use the simpler CacheModule.register() method:

typescript
import { CacheModule } from '@nestjs/cache-manager';

@Module({
  imports: [CacheModule.register()], // In-memory, default settings
})
export class AppModule {}

For production with Redis:

typescript
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';
import { Keyv } from 'keyv';
import { CacheableMemory } from 'cacheable';

CacheModule.registerAsync({
  useFactory: async () => ({
    stores: [
      new Keyv({
        store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }),
      }),
      new KeyvRedis('redis://localhost:6379'),
    ],
  }),
})

Cache Aside Pattern

typescript
@Injectable()
export class ProductsService {
  constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    private productsRepo: Repository<Product>,
  ) {}

  async findById(id: string): Promise<Product> {
    const cacheKey = `product:${id}`;

    // Check cache first
    const cached = await this.cacheManager.get<Product>(cacheKey);
    if (cached) return cached;

    // Cache miss: fetch from database
    const product = await this.productsRepo.findOne({ where: { id } });
    if (!product) throw new NotFoundException();

    // Store in cache
    await this.cacheManager.set(cacheKey, product, 3600000); // 1 hour

    return product;
  }

  async update(id: string, dto: UpdateProductDto): Promise<Product> {
    await this.productsRepo.update(id, dto);
    const product = await this.productsRepo.findOne({ where: { id } });

    // Invalidate cache
    await this.cacheManager.del(`product:${id}`);

    return product;
  }
}

Multi-Level Caching

typescript
@Injectable()
export class MultiLevelCacheService {
  private localCache = new Map<string, { value: any; expiry: number }>();

  constructor(@Inject(CACHE_MANAGER) private redis: Cache) {}

  async get<T>(key: string): Promise<T | null> {
    // L1: Local memory cache
    const local = this.localCache.get(key);
    if (local && local.expiry > Date.now()) {
      return local.value;
    }

    // L2: Redis cache
    const remote = await this.redis.get<T>(key);
    if (remote) {
      // Populate L1 cache
      this.localCache.set(key, {
        value: remote,
        expiry: Date.now() + 60000, // 1 minute local cache
      });
      return remote;
    }

    return null;
  }

  async set(key: string, value: any, ttl: number) {
    await this.redis.set(key, value, ttl);
    this.localCache.set(key, {
      value,
      expiry: Date.now() + Math.min(ttl, 60000),
    });
  }
}

Cache Warming

typescript
@Injectable()
export class CacheWarmingService implements OnApplicationBootstrap {
  async onApplicationBootstrap() {
    await this.warmCache();
  }

  @Cron('0 */30 * * * *')  // Every 30 minutes
  async warmCache() {
    // Pre-populate frequently accessed data
    const popularProducts = await this.productsRepo.find({
      where: { featured: true },
    });

    for (const product of popularProducts) {
      await this.cacheManager.set(
        `product:${product.id}`,
        product,
        3600000,
      );
    }

    this.logger.log(`Warmed cache with ${popularProducts.length} products`);
  }
}

Cache Tags for Invalidation

typescript
@Injectable()
export class TaggedCacheService {
  private tags = new Map<string, Set<string>>();

  async setWithTags(key: string, value: any, tags: string[], ttl: number) {
    await this.cacheManager.set(key, value, ttl);

    for (const tag of tags) {
      if (!this.tags.has(tag)) {
        this.tags.set(tag, new Set());
      }
      this.tags.get(tag).add(key);
    }
  }

  async invalidateTag(tag: string) {
    const keys = this.tags.get(tag);
    if (!keys) return;

    for (const key of keys) {
      await this.cacheManager.del(key);
    }
    this.tags.delete(tag);
  }
}

// Usage
await this.cache.setWithTags(
  `product:${id}`,
  product,
  ['products', `category:${product.categoryId}`],
  3600000,
);

// Invalidate all products in a category
await this.cache.invalidateTag(`category:${categoryId}`);

HTTP Response Caching

typescript
@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
  @Get()
  @CacheTTL(300000)  // 5 minutes
  findAll() {
    return this.productsService.findAll();
  }

  @Get(':id')
  @CacheKey('product')  // Custom cache key
  @CacheTTL(600000)  // 10 minutes
  findOne(@Param('id') id: string) {
    return this.productsService.findById(id);
  }
}

Common Pitfalls

  1. Stale data: Cache invalidation on updates is crucial.
  2. Cache stampede: Many requests miss cache simultaneously.
  3. Over-caching: Caching volatile data wastes memory.

Best Practices

  • Cache expensive computations and frequent queries
  • Use appropriate TTLs based on data volatility
  • Implement cache invalidation on data changes
  • Use multi-level caching for high-traffic data
  • Monitor cache hit rates

Summary

Caching strategies include cache-aside, write-through, and multi-level approaches. Use TTLs for automatic expiration, invalidation on updates, and cache warming for critical data. Monitor hit rates to tune effectiveness.

Code Examples

typescript
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';
import { Keyv } from 'keyv';
import { CacheableMemory } from 'cacheable';

CacheModule.registerAsync({
  useFactory: async () => ({
    stores: [
      new Keyv({
        store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }),
      }),
      new KeyvRedis('redis://localhost:6379'),
    ],
  }),
});
✓ Completed