We use cookies to enhance your experience on the site
CodeWorlds

Caching - supply stashes in the application

Supply manager! Consul Caesar.js noticed that the legion keeps running to the storehouse for the same items. It's time to organize strategic stashes with the most frequently used supplies in easily accessible places. In the NestJS world, this means implementing a caching system!

What is caching in the world of legionaries?

Imagine that your legion constantly needs:

  • Maps of the most frequently visited islands - instead of going down to the cartographic cabin
  • Lists of legion members - instead of checking the main registry
  • Weather data - instead of searching through navigation journals
  • Tribute information - instead of searching through all documentation

Cache is like small stashes placed around the fort with copies of the most important information!

Basic caching in NestJS

1// cache.module.ts
2import { CacheModule } from '@nestjs/cache-manager';
3import { Module } from '@nestjs/common';
4
5@Module({
6 imports: [
7 CacheModule.register({
8 ttl: 300, // 5 minuteses
9 max: 100, // maximum 100 elements
10 }),
11 ],
12})
13export class LegionaryCacheModule {}
14
15// tribute.service.ts
16import { CACHE_MANAGER } from '@nestjs/cache-manager';
17import { Cache } from 'cache-manager';
18import { Injectable, Inject } from '@nestjs/common';
19
20@Injectable()
21export class TributeService {
22 constructor(
23 @Inject(CACHE_MANAGER) private cacheManager: Cache,
24 private tributeRepository: TributeRepository,
25 ) {}
26
27 async findTributeById(id: number): Promise<Tribute> {
28 const cacheKey = `tribute:${id}`;
29
30 // Check cache
31 let tribute = await this.cacheManager.get<Tribute>(cacheKey);
32 
33 if (tribute) {
34 console.log(' Tribute found in the stash!');
35 return tribute;
36 }
37 
38 // Fetch from the database
39 console.log(' Searching for tribute in the main treasury...');
40 tribute = await this.tributeRepository.findOne({ where: { id } });
41 
42 if (tribute) {
43 // Save in cache for 10 minutes
44 await this.cacheManager.set(cacheKey, tribute, 600);
45 console.log(' Tribute added to the stash!');
46 }
47 
48 return tribute;
49 }
50
51 async updateTribute(id: number, data: Partial<Tribute>): Promise<Tribute> {
52 const tribute = await this.tributeRepository.update(id, data);
53 
54 // Remove from cache after update
55 const cacheKey = `tribute:${id}`;
56 await this.cacheManager.del(cacheKey);
57 console.log('Removed outdated data from the stash');
58 
59 return tribute;
60 }
61
62 async getTributesByType(type: string): Promise<Tribute[]> {
63 const cacheKey = `tributes:type:${type}`;
64 
65 let tributes = await this.cacheManager.get<Tribute[]>(cacheKey);
66 
67 if (!tributes) {
68 tributes = await this.tributeRepository.find({ 
69 where: { type } 
70 });
71 
72 // Cache for 5 minutes
73 await this.cacheManager.set(cacheKey, tributes, 300);
74 }
75 
76 return tributes;
77 }
78}

Cache Decorator - automating stashes

1// decorators/cacheable.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4export const CACHE_KEY_METADATA = 'cache_key';
5export const CACHE_TTL_METADATA = 'cache_ttl';
6
7export const Cacheable = (key?: string, ttl?: number) => {
8 return (target: any, propertyName: string, descriptor: PropertyDescriptor) => {
9 SetMetadata(CACHE_KEY_METADATA, key || propertyName)(target, propertyName, descriptor);
10 if (ttl) {
11 SetMetadata(CACHE_TTL_METADATA, ttl)(target, propertyName, descriptor);
12 }
13 };
14};
15
16// cache.interceptor.ts
17import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Inject } from '@nestjs/common';
18import { Reflector } from '@nestjs/core';
19import { CACHE_MANAGER } from '@nestjs/cache-manager';
20import { Cache } from 'cache-manager';
21import { Observable, of } from 'rxjs';
22import { tap } from 'rxjs/operators';
23
24@Injectable()
25export class CacheInterceptor implements NestInterceptor {
26 constructor(
27 @Inject(CACHE_MANAGER) private cacheManager: Cache,
28 private reflector: Reflector,
29 ) {}
30
31 async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
32 const cacheKey = this.reflector.get<string>(CACHE_KEY_METADATA, context.getHandler());
33 const ttl = this.reflector.get<number>(CACHE_TTL_METADATA, context.getHandler()) || 300;
34 
35 if (!cacheKey) {
36 return next.handle();
37 }
38
39 const request = context.switchToHttp().getRequest();
40 const fullCacheKey = this.generateCacheKey(cacheKey, request);
41 
42 // Check cache
43 const cachedResult = await this.cacheManager.get(fullCacheKey);
44 if (cachedResult) {
45 console.log(` Data retrieved from stash: ${fullCacheKey}`);
46 return of(cachedResult);
47 }
48
49 // Execute the operation and save in cache
50 return next.handle().pipe(
51 tap(async (result) => {
52 await this.cacheManager.set(fullCacheKey, result, ttl);
53 console.log(` Data saved to stash: ${fullCacheKey}`);
54 }),
55 );
56 }
57
58 private generateCacheKey(baseKey: string, request: any): string {
59 const params = { ...request.params, ...request.query };
60 const paramString = Object.keys(params)
61 .sort()
62 .map(key => `${key}:${params[key]}`)
63 .join(':');
64 
65 return paramString ? `${baseKey}:${paramString}` : baseKey;
66 }
67}
68
69// Usage in a service
70@Injectable()
71export class LegionService {
72 @Cacheable('legion_stats', 600) // 10 minutes
73 async getLegionStatistics(legionId: number) {
74 // Complex calculations...
75 return {
76 totalLegions: 25,
77 totalLegion: 1250,
78 totalTributes: 5000,
79 averageSpeed: 12.5,
80 };
81 }
82
83 @Cacheable('legion_list', 300) // 5 minuteses
84 async getLegionByCohort(cohortId: number) {
85 // Database query...
86 return this.legionRepository.find({ where: { cohortId } });
87 }
88}

Cache Warmer - preparing stashes

1// cache-warmer.service.ts
2@Injectable()
3export class CacheWarmerService {
4 constructor(
5 @Inject(CACHE_MANAGER) private cacheManager: Cache,
6 private tributeService: TributeService,
7 private legionService: LegionService,
8 private weatherService: WeatherService,
9 ) {}
10
11 @Cron('0 */30 * * * *') // Every 30 minutes
12 async warmPopularCaches() {
13 console.log('Preparing popular stashes...');
14
15 try {
16 // Most popular tributes
17 const popularTributeIds = [1, 2, 3, 5, 8, 13, 21];
18 await Promise.all(
19 popularTributeIds.map(id => 
20 this.tributeService.findTributeById(id)
21 )
22 );
23
24 // Statistics for all legions
25 const legionIds = await this.legionService.getAllLegionIds();
26 await Promise.all(
27 legionIds.map(id => 
28 this.legionService.getLegionStatistics(id)
29 )
30 );
31
32 // Weather forecast for main routes
33 const mainRoutes = ['Hispania', 'mediterranean', 'north_sea'];
34 await Promise.all(
35 mainRoutes.map(route => 
36 this.weatherService.getWeatherForecast(route)
37 )
38 );
39
40 console.log('✅ Stashes prepared!');
41 } catch (error) {
42 console.error('❌ Error while preparing stashes:', error);
43 }
44 }
45
46 async warmCacheForUser(userId: number) {
47 // Prepare cache for a specific user
48 const user = await this.userService.findById(userId);
49 
50 if (user.favoriteLegionId) {
51 await this.legionService.getLegionDetails(user.favoriteLegionId);
52 }
53 
54 if (user.recentTributes?.length) {
55 await Promise.all(
56 user.recentTributes.map(tributeId =>
57 this.tributeService.findTributeById(tributeId)
58 )
59 );
60 }
61 }
62
63 async preloadDashboardData() {
64 // Data for the main dashboard
65 const dashboardPromises = [
66 this.cacheManager.set('dashboard:total_legions', await this.getLegionCount(), 3600),
67 this.cacheManager.set('dashboard:active_cohorts', await this.getActiveLegionCount(), 1800),
68 this.cacheManager.set('dashboard:recent_tributes', await this.getRecentTributes(), 900),
69 this.cacheManager.set('dashboard:weather_alerts', await this.getWeatherAlerts(), 600),
70 ];
71
72 await Promise.all(dashboardPromises);
73 console.log(' Dashboard cache loaded!');
74 }
75}

Cache Invalidation - cleaning expired stashes

1// cache-invalidation.service.ts
2@Injectable()
3export class CacheInvalidationService {
4 constructor(
5 @Inject(CACHE_MANAGER) private cacheManager: Cache,
6 ) {}
7
8 async invalidateTributeCache(tributeId: number) {
9 const patterns = [
10 `tribute:${tributeId}`,
11 `tributes:*`, // All tribute lists
12 `dashboard:recent_tributes`,
13 ];
14
15 await this.invalidateByPatterns(patterns);
16 console.log(`Cache cleared for tribute ${tributeId}`);
17 }
18
19 async invalidateLegionCache(legionId: number) {
20 const patterns = [
21 `legion:${legionId}:*`,
22 `legion_stats:${legionId}`,
23 `dashboard:*`,
24 ];
25
26 await this.invalidateByPatterns(patterns);
27 }
28
29 async invalidateUserCache(userId: number) {
30 const patterns = [
31 `user:${userId}:*`,
32 `legion_list:*`,
33 ];
34
35 await this.invalidateByPatterns(patterns);
36 }
37
38 private async invalidateByPatterns(patterns: string[]) {
39 // In a real implementation with Redis, use SCAN + DEL
40 for (const pattern of patterns) {
41 if (pattern.includes('*')) {
42 // Delete all keys matching the pattern
43 await this.deleteByPattern(pattern);
44 } else {
45 // Delete a specific key
46 await this.cacheManager.del(pattern);
47 }
48 }
49 }
50
51 private async deleteByPattern(pattern: string) {
52 // Implementation depends on the cache manager used
53 // For in-memory cache:
54 const keys = await this.cacheManager.store.keys();
55 const matchingKeys = keys.filter(key => this.matchPattern(key, pattern));
56 
57 await Promise.all(
58 matchingKeys.map(key => this.cacheManager.del(key))
59 );
60 }
61
62 private matchPattern(key: string, pattern: string): boolean {
63 const regex = new RegExp(pattern.replace('*', '.*'));
64 return regex.test(key);
65 }
66}
67
68// Event-driven cache invalidation
69@Injectable()
70export class TributeEventHandler {
71 constructor(
72 private cacheInvalidation: CacheInvalidationService,
73 ) {}
74
75 @OnEvent('tribute.created')
76 async handleTributeCreated(event: TributeCreatedEvent) {
77 await this.cacheInvalidation.invalidateTributeCache(event.tributeId);
78 console.log('Cache invalidated after adding a tribute');
79 }
80
81 @OnEvent('tribute.updated')
82 async handleTributeUpdated(event: TributeUpdatedEvent) {
83 await this.cacheInvalidation.invalidateTributeCache(event.tributeId);
84 console.log('Cache invalidated after updating a tribute');
85 }
86
87 @OnEvent('legion.disbanded')
88 async handleLegionDisbanded(event: LegionDisbandedEvent) {
89 await this.cacheInvalidation.invalidateLegionCache(event.legionId);
90 console.log(' Cache invalidated after resolving the conflict');
91 }
92}

Cache Metrics - monitoring stashes

1// cache-metrics.service.ts
2@Injectable()
3export class CacheMetricsService {
4 private metrics = {
5 hits: 0,
6 misses: 0,
7 sets: 0,
8 deletes: 0,
9 errors: 0,
10 };
11
12 constructor(
13 @Inject(CACHE_MANAGER) private cacheManager: Cache,
14 ) {}
15
16 recordHit() {
17 this.metrics.hits++;
18 }
19
20 recordMiss() {
21 this.metrics.misses++;
22 }
23
24 recordSet() {
25 this.metrics.sets++;
26 }
27
28 recordDelete() {
29 this.metrics.deletes++;
30 }
31
32 recordError() {
33 this.metrics.errors++;
34 }
35
36 getMetrics() {
37 const total = this.metrics.hits + this.metrics.misses;
38 const hitRate = total > 0 ? (this.metrics.hits / total) * 100 : 0;
39
40 return {
41 ...this.metrics,
42 hitRate: hitRate.toFixed(2) + '%',
43 totalRequests: total,
44 };
45 }
46
47 @Cron('0 */15 * * * *') // Every 15 minutes
48 logMetrics() {
49 const metrics = this.getMetrics();
50 console.log('Cache Metrics:', {
51 hitRate: metrics.hitRate,
52 totalRequests: metrics.totalRequests,
53 errors: metrics.errors,
54 });
55
56 // Reset metrics every hour
57 if (new Date().getMinutes() === 0) {
58 this.resetMetrics();
59 }
60 }
61
62 private resetMetrics() {
63 this.metrics = {
64 hits: 0,
65 misses: 0,
66 sets: 0,
67 deletes: 0,
68 errors: 0,
69 };
70 }
71
72 async getCacheSize(): Promise<number> {
73 try {
74 const keys = await this.cacheManager.store.keys();
75 return keys.length;
76 } catch (error) {
77 return -1;
78 }
79 }
80
81 async getMemoryUsage(): Promise<string> {
82 // Implementation depends on the cache type
83 const used = process.memoryUsage();
84 return `${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`;
85 }
86}

Cache Configuration per Environment

1// cache.config.ts
2import { ConfigService } from '@nestjs/config';
3
4export const cacheConfigFactory = (configService: ConfigService) => {
5 const environment = configService.get('NODE_ENV');
6 
7 switch (environment) {
8 case 'production':
9 return {
10 ttl: 3600, // 1 hour
11 max: 1000, // 1000 elements
12 store: 'redis',
13 host: configService.get('REDIS_HOST'),
14 port: configService.get('REDIS_PORT'),
15 };
16 
17 case 'staging':
18 return {
19 ttl: 1800, // 30 minuteses
20 max: 500,
21 store: 'redis',
22 host: configService.get('REDIS_HOST'),
23 port: configService.get('REDIS_PORT'),
24 };
25 
26 default: // development
27 return {
28 ttl: 300, // 5 minuteses
29 max: 100,
30 store: 'memory',
31 };
32 }
33};
34
35// Usage in the module
36@Module({
37 imports: [
38 CacheModule.registerAsync({
39 imports: [ConfigModule],
40 useFactory: cacheConfigFactory,
41 inject: [ConfigService],
42 }),
43 ],
44})
45export class AppModule {}

Caching best practices

  1. Cache only stable data - don't cache data that changes every second
  2. Set an appropriate TTL - short for frequently changing data, long for static data
  3. Use cache invalidation - remove outdated data after changes
  4. Monitor hit rate - should be above 80%
  5. Cache warm-up - prepare popular data in advance
  6. Graceful degradation - the application must work without cache

Cache is a powerful weapon in every legionary-programmer's arsenal! Well-organized stashes can speed up a fort up to 10 times. Remember though - cache is not a universal solution for all performance problems!

Go to CodeWorlds