Advanced Caching Strategies Guide

This guide covers advanced caching patterns and strategies for building high-performance applications with HazelJS. You'll learn when and how to use different caching approaches, from basic method caching to sophisticated distributed locking and cache warming strategies.

Table of Contents

Understanding Caching Patterns

Caching is not one-size-fits-all. Different scenarios require different caching strategies. Let's explore the most common patterns and when to use them.

Cache Patterns Overview

PatternUse CaseConsistencyPerformanceComplexity
Cache-AsideRead-heavy workloadsEventualHighLow
Write-ThroughCritical dataStrongMediumLow
Write-BehindHigh-volume writesEventualVery HighMedium
Cache-LockExpensive computationsStrongHighMedium
Cache-WarmingPredictable accessEventualVery HighHigh

Choosing the Right Pattern

// Decision matrix for caching patterns
const chooseCachingPattern = (scenario: string) => {
  switch (scenario) {
    case 'user-profile-reads':
      return 'Cache-Aside'; // High read-to-write ratio
    case 'financial-transactions':
      return 'Write-Through'; // Strong consistency required
    case 'analytics-events':
      return 'Write-Behind'; // High write volume, eventual consistency OK
    case 'report-generation':
      return 'Cache-Lock + Cache-Warming'; // Expensive, predictable access
    default:
      return 'Cache-Aside'; // Default safe choice
  }
};

Basic Caching Setup

Before diving into advanced patterns, let's set up a solid foundation.

Step 1: Install and Configure

npm install @hazeljs/cache
# For Redis support
npm install ioredis

Step 2: Module Configuration

// src/app.module.ts
import { HazelModule } from '@hazeljs/core';
import { CacheModule } from '@hazeljs/cache';

@HazelModule({
  imports: [
    CacheModule.forRoot({
      strategy: 'multi-tier', // Best for production
      redis: {
        host: process.env.REDIS_HOST || 'localhost',
        port: parseInt(process.env.REDIS_PORT || '6379'),
        password: process.env.REDIS_PASSWORD,
      },
      ttl: 3600, // Default 1 hour
      cleanupInterval: 60000, // Cleanup every minute
    }),
  ],
})
export class AppModule {}

Step 3: Basic Service Setup

// src/services/base.service.ts
import { Service } from '@hazeljs/core';
import { CacheService } from '@hazeljs/cache';

@Service()
export class BaseService {
  constructor(protected readonly cache: CacheService) {}

  protected async getOrSet<T>(
    key: string,
    fetcher: () => Promise<T>,
    ttl: number = 3600,
    tags: string[] = []
  ): Promise<T> {
    return await this.cache.getOrSet(key, fetcher, ttl, tags);
  }

  protected async invalidate(tags: string[]): Promise<void> {
    await this.cache.invalidateTags(tags);
  }
}

Cache-Aside Pattern

The cache-aside pattern is the most common and versatile caching strategy. It's perfect for read-heavy workloads where eventual consistency is acceptable.

When to Use Cache-Aside

  • Read-heavy applications: More reads than writes (3:1 ratio or higher)
  • User data: Profiles, preferences, settings
  • Product catalogs: E-commerce product information
  • Configuration data: Application settings, feature flags

Implementation Examples

Basic Cache-Aside

// src/services/user.service.ts
import { Service } from '@hazeljs/core';
import { CacheAside } from '@hazeljs/cache';

@Service()
export class UserService extends BaseService {
  @CacheAside({
    key: 'user-{id}',
    ttl: 1800, // 30 minutes
  })
  async getUser(id: string) {
    console.log(`📥 Fetching user ${id} from database`);
    return await this.db.user.findUnique({ where: { id } });
  }

  @CacheAside({
    key: 'user-profile-{id}',
    ttl: 3600, // 1 hour
  })
  async getUserProfile(id: string) {
    const user = await this.getUser(id);
    const profile = await this.db.profile.findUnique({ 
      where: { userId: id } 
    });
    return { user, profile };
  }
}

Cache-Aside with Fallback

// src/services/product.service.ts
import { CacheAsideWithFallback } from '@hazeljs/cache';

@Service()
export class ProductService extends BaseService {
  @CacheAsideWithFallback({
    key: 'product-{id}',
    ttl: 3600,
    fallbackValue: {
      id: 'unknown',
      name: 'Product Not Found',
      price: 0,
      available: false
    }
  })
  async getProduct(id: string) {
    return await this.db.product.findUnique({ where: { id } });
  }

  // Dynamic fallback based on context
  @CacheAside({
    key: 'product-recommendations-{userId}',
    ttl: 7200, // 2 hours
    fallback: async function(this: ProductService, userId: string) {
      // Get user's category preferences
      const user = await this.getUser(userId);
      const category = user.preferredCategory || 'electronics';
      
      // Return generic recommendations for that category
      return await this.getGenericRecommendations(category);
    }
  })
  async getRecommendations(userId: string) {
    return await this.recommendationEngine.getPersonalized(userId);
  }
}

Cache-Aside Best Practices

  • Use descriptive keys: user-{id}, product-{slug}, config-{key}
  • Set appropriate TTLs: Shorter for frequently changing data
  • Use tags for invalidation: Group related cache entries
  • Handle cache failures gracefully: Always have fallback logic

Write Strategies

Write strategies determine how you handle cache updates when data changes. The choice between write-through and write-behind depends on your consistency requirements and performance needs.

Write-Through Caching

Write-through caching updates the cache immediately when data changes. This provides strong consistency but can impact write performance.

When to Use Write-Through

  • Critical data: User authentication, financial transactions
  • Real-time requirements: Chat messages, live updates
  • Consistency over performance: When stale data is unacceptable

Implementation

// src/services/account.service.ts
import { WriteThrough } from '@hazeljs/cache';

@Service()
export class AccountService extends BaseService {
  @WriteThrough({
    key: 'account-{id}',
    ttl: 3600,
  })
  async updateAccount(id: string, data: UpdateAccountDto) {
    console.log(`✏️ Updating account ${id} in database and cache`);
    
    // 1. Update database
    const updated = await this.db.account.update({
      where: { id },
      data: {
        ...data,
        updatedAt: new Date()
      }
    });
    
    // 2. Cache is automatically updated by decorator
    return updated;
  }

  @WriteThrough({
    key: 'user-balance-{userId}',
    ttl: 300, // 5 minutes - financial data should be fresh
  })
  async updateBalance(userId: string, amount: number) {
    // Financial update - must be immediately consistent
    const updated = await this.db.account.update({
      where: { userId },
      data: { balance: { increment: amount } }
    });
    
    // Cache updated immediately with new balance
    return updated;
  }
}

Write-Behind Caching

Write-behind caching queues cache updates for asynchronous processing. This provides excellent write performance with eventual consistency.

When to Use Write-Behind

  • High-volume writes: Analytics events, logging
  • Performance-critical writes: Real-time gaming, IoT data
  • Eventual consistency is acceptable: Non-critical data

Implementation

// src/services/analytics.service.ts
import { WriteBehind } from '@hazeljs/cache';

@Service()
export class AnalyticsService extends BaseService {
  @WriteBehind({
    key: 'page-views-{url}',
    ttl: 300, // 5 minutes
    async: true,
  })
  async recordPageView(url: string, userId?: string) {
    console.log(`📊 Recording page view for ${url}`);
    
    // Update database immediately
    const updated = await this.db.pageView.upsert({
      where: { url },
      update: { 
        count: { increment: 1 },
        lastViewed: new Date()
      },
      create: { 
        url, 
        count: 1, 
        lastViewed: new Date() 
      }
    });
    
    // Cache update happens asynchronously
    return updated;
  }

  @WriteBehind({
    key: 'user-events-{userId}',
    ttl: 1800, // 30 minutes
    async: true,
  })
  async trackUserEvent(userId: string, event: UserEvent) {
    // High-frequency event tracking
    const updated = await this.db.userEvent.create({
      data: {
        userId,
        type: event.type,
        data: event.data,
        timestamp: new Date()
      }
    });
    
    // Cache updated asynchronously for better performance
    return updated;
  }

  // Batch write-behind for bulk operations
  @WriteBehind({
    key: 'batch-metrics-{batchId}',
    ttl: 3600,
    async: true,
  })
  async processBatchMetrics(batchId: string, metrics: Metric[]) {
    // Process large batch of metrics
    const results = await Promise.all(
      metrics.map(metric => 
        this.db.metric.create({
          data: { ...metric, batchId }
        })
      )
    );
    
    return { processed: results.length, batchId };
  }
}

Write Strategy Comparison

AspectWrite-ThroughWrite-Behind
ConsistencyStrongEventual
Write PerformanceGoodExcellent
Cache FreshnessImmediateDelayed
ComplexityLowMedium
Use CaseCritical dataHigh-volume writes

Distributed Locking

Distributed locking prevents cache stampede and ensures expensive operations run only once across multiple instances.

When to Use Distributed Locking

  • Expensive computations: Report generation, data analysis
  • Rate-limited APIs: External service calls with limits
  • Resource-intensive operations: File processing, image generation
  • Preventing thundering herd: Multiple requests for same data

Implementation Examples

Basic Cache Lock

// src/services/report.service.ts
import { CacheLock } from '@hazeljs/cache';

@Service()
export class ReportService extends BaseService {
  @CacheLock({
    key: 'report-{type}-{date}',
    ttl: 60000, // 1 minute lock
    retryDelay: 2000, // Check every 2 seconds
    maxRetries: 10, // Try up to 10 times (20 seconds total)
  })
  @Cache({
    key: 'report-{type}-{date}',
    ttl: 7200, // Cache result for 2 hours
    tags: ['reports', 'reports-{type}']
  })
  async generateReport(type: string, date: string) {
    console.log(`🔒 Generating ${type} report for ${date}`);
    
    // Expensive report generation
    await new Promise(resolve => setTimeout(resolve, 10000));
    
    const report = {
      type,
      date,
      data: `Report data for ${type} on ${date}`,
      generatedAt: new Date().toISOString(),
      size: Math.floor(Math.random() * 10000) // Simulate report size
    };
    
    return report;
  }

  @CacheLock({
    key: 'export-{format}-{userId}',
    ttl: 300000, // 5 minutes for large exports
    retryDelay: 5000,
    maxRetries: 6, // 30 seconds total wait time
  })
  async exportUserData(format: 'csv' | 'json' | 'pdf', userId: string) {
    console.log(`📤 Exporting user ${userId} data as ${format}`);
    
    // Simulate large data export
    const userData = await this.db.user.findUnique({ where: { id: userId } });
    const userActions = await this.db.userAction.findMany({ 
      where: { userId },
      take: 10000
    });
    
    // Process export based on format
    let exportData;
    switch (format) {
      case 'csv':
        exportData = this.generateCSV(userData, userActions);
        break;
      case 'json':
        exportData = this.generateJSON(userData, userActions);
        break;
      case 'pdf':
        exportData = await this.generatePDF(userData, userActions);
        break;
    }
    
    return {
      userId,
      format,
      size: exportData.length,
      downloadUrl: `/downloads/${userId}-export.${format}`,
      expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
    };
  }
}

Advanced Lock Patterns

// src/services/ai.service.ts
@Service()
export class AIService extends BaseService {
  @CacheLock({
    key: 'ai-analysis-{type}-{hash}',
    ttl: 120000, // 2 minutes for AI processing
    retryDelay: 3000,
    maxRetries: 20, // 1 minute total wait
  })
  @Cache({
    key: 'ai-analysis-{type}-{hash}',
    ttl: 14400, // 4 hours cache for AI results
    tags: ['ai-analysis', 'ai-{type}']
  })
  async analyzeContent(type: 'sentiment' | 'classification' | 'extraction', content: string) {
    console.log(`🤖 Running AI ${type} analysis`);
    
    // Generate hash for content to avoid duplicate analysis
    const hash = this.createHash(content);
    
    // Call external AI service (rate limited and expensive)
    const result = await this.aiProvider.analyze({
      type,
      content,
      model: 'gpt-4'
    });
    
    return {
      type,
      contentHash: hash,
      result,
      confidence: result.confidence,
      processedAt: new Date().toISOString()
    };
  }

  @CacheLock({
    key: 'batch-processing-{batchId}',
    ttl: 600000, // 10 minutes for batch processing
    retryDelay: 10000,
    maxRetries: 6, // 1 minute total
  })
  async processBatch(batchId: string) {
    console.log(`🔄 Processing batch ${batchId}`);
    
    const batch = await this.db.batch.findUnique({ where: { id: batchId } });
    if (!batch) throw new Error('Batch not found');
    
    // Process all items in batch
    const results = await Promise.all(
      batch.items.map(async (item, index) => {
        console.log(`Processing item ${index + 1}/${batch.items.length}`);
        return await this.processItem(item);
      })
    );
    
    // Update batch status
    await this.db.batch.update({
      where: { id: batchId },
      data: { 
        status: 'completed',
        completedAt: new Date(),
        resultCount: results.length
      }
    });
    
    return { batchId, processed: results.length };
  }

  private createHash(content: string): string {
    const crypto = require('crypto');
    const algorithm = 'sha256';
    return crypto.createHash(algorithm).update(content).digest('hex').substring(0, 16);
  }
}

Lock Management Best Practices

  • Set appropriate lock TTLs: Long enough for operation, short enough to prevent deadlocks
  • Configure retry strategies: Balance between responsiveness and resource usage
  • Monitor lock contention: High contention may indicate performance issues
  • Handle lock failures gracefully: Provide fallback or retry logic

Cache Warming

Cache warming pre-populates cache with frequently accessed data, improving performance for predictable access patterns.

When to Use Cache Warming

  • Predictable access patterns: Daily reports, popular products
  • Expensive computations: Complex analytics, AI processing
  • Performance-critical applications: E-commerce, real-time dashboards
  • Scheduled operations: Batch jobs, periodic reports

Implementation Examples

Scheduled Cache Warming

// src/services/dashboard.service.ts
import { CacheWarm, CacheWarmingUtils } from '@hazeljs/cache';

@Service()
export class DashboardService extends BaseService {
  @CacheWarm({
    keys: [
      'dashboard-overview',
      'sales-summary',
      'user-metrics',
      'product-performance'
    ],
    fetcher: async function(this: DashboardService, key: string) {
      console.log(`🔥 Warming dashboard cache: ${key}`);
      
      switch (key) {
        case 'dashboard-overview':
          return await this.getOverviewData();
        case 'sales-summary':
          return await this.getSalesSummary();
        case 'user-metrics':
          return await this.getUserMetrics();
        case 'product-performance':
          return await this.getProductPerformance();
        default:
          return null;
      }
    },
    ttl: 3600, // 1 hour
    parallel: true, // Warm all keys in parallel
    schedule: '0 8 * * *', // Every day at 8 AM
    condition: 'business-hours' // Only during business hours
  })
  async warmDashboardCache() {
    console.log('🌡️ Dashboard cache warming completed');
    return { message: 'Dashboard cache warmed successfully' };
  }

  private async getOverviewData() {
    // Expensive overview query
    const [users, products, orders, revenue] = await Promise.all([
      this.db.user.count(),
      this.db.product.count(),
      this.db.order.count({ where: { status: 'completed' } }),
      this.db.order.aggregate({ _sum: { total: true } })
    ]);
    
    return { users, products, orders, revenue: revenue._sum.total };
  }

  private async getSalesSummary() {
    const today = new Date();
    const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
    
    return await this.db.order.groupBy({
      by: ['status'],
      where: {
        createdAt: { gte: lastMonth }
      },
      _sum: { total: true },
      _count: true
    });
  }

  private async getUserMetrics() {
    return await this.db.user.aggregate({
      _count: true,
      _avg: { rating: true },
      where: { isActive: true }
    });
  }

  private async getProductPerformance() {
    return await this.db.product.findMany({
      orderBy: { salesCount: 'desc' },
      take: 10,
      select: {
        id: true,
        name: true,
        salesCount: true,
        rating: true,
        category: true
      }
    });
  }
}

Dynamic Cache Warming

// src/services/content.service.ts
@Service()
export class ContentService extends BaseService {
  @CacheWarm({
    keys: [
      'trending-articles',
      'featured-content',
      'popular-categories'
    ],
    fetcher: async function(this: ContentService, key: string) {
      console.log(`🔥 Warming content cache: ${key}`);
      
      switch (key) {
        case 'trending-articles':
          return await this.getTrendingArticles();
        case 'featured-content':
          return await this.getFeaturedContent();
        case 'popular-categories':
          return await this.getPopularCategories();
        default:
          return null;
      }
    },
    ttl: 7200, // 2 hours
    parallel: true,
    schedule: '*/30 * * * *', // Every 30 minutes
    condition: 'high-traffic' // Only during high traffic periods
  })
  async warmContentCache() {
    return { message: 'Content cache warmed' };
  }

  // Context-aware warming based on user behavior
  @CacheWarm({
    keys: async function(this: ContentService) {
      // Dynamically determine keys based on recent activity
      const recentViews = await this.getRecentViews();
      const trendingTopics = await this.getTrendingTopics();
      
      return [
        ...recentViews.map(view => `article-${view.articleId}`),
        ...trendingTopics.map(topic => `topic-${topic.id}-articles`)
      ];
    },
    fetcher: async function(this: ContentService, key: string) {
      if (key.startsWith('article-')) {
        const articleId = key.replace('article-', '');
        return await this.getArticle(articleId);
      } else if (key.startsWith('topic-')) {
        const parts = key.split('-');
        const topicId = parts[1];
        return await this.getTopicArticles(topicId);
      }
    },
    ttl: 3600,
    parallel: true,
    schedule: '0,15,30,45 * * * *' // Every 15 minutes
  })
  async warmDynamicContent() {
    return { message: 'Dynamic content warmed' };
  }

  private async getRecentViews() {
    const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
    return await this.db.view.findMany({
      where: { createdAt: { gte: oneHourAgo } },
      select: { articleId: true },
      distinct: ['articleId'],
      take: 50
    });
  }

  private async getTrendingTopics() {
    return await this.db.topic.findMany({
      orderBy: { trendingScore: 'desc' },
      take: 10
    });
  }
}

Manual Cache Warming

// src/controllers/admin.controller.ts
import { Controller, Post, Get, Body } from '@hazeljs/core';
import { CacheWarmingUtils } from '@hazeljs/cache';

@Controller('admin/cache')
export class CacheController {
  @Post('warm')
  async warmCache(@Body() body: { job?: string }) {
    try {
      const jobs = CacheWarmingUtils.listJobs();
      
      if (body.job) {
        // Warm specific job
        const job = jobs.find(j => j.name === body.job);
        if (!job) {
          return { success: false, message: 'Job not found' };
        }
        
        await CacheWarmingUtils.warmUp(job);
        return { success: true, message: `Warmed job: ${body.job}` };
      } else {
        // Warm all jobs
        await Promise.all(jobs.map(job => CacheWarmingUtils.warmUp(job)));
        return { success: true, message: `Warmed ${jobs.length} jobs` };
      }
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  @Get('jobs')
  listWarmingJobs() {
    return {
      jobs: CacheWarmingUtils.listJobs(),
      stats: CacheWarmingUtils.getStats()
    };
  }

  @Post('clear')
  clearWarmingJobs() {
    CacheWarmingUtils.destroy();
    return { success: true, message: 'Warming jobs cleared' };
  }
}

Cache Warming Strategies

  • Scheduled Warming: Use cron patterns for predictable warming
  • Conditional Warming: Warm based on traffic, time, or business conditions
  • Dynamic Warming: Generate keys based on current usage patterns
  • Manual Warming: Provide admin controls for on-demand warming

Multi-Tier Caching

Multi-tier caching combines multiple cache layers for optimal performance and cost-effectiveness.

Architecture Overview

Application
    ↓
L-1 Cache (Memory) - Fastest, smallest
    ↓ (miss)
L-2 Cache (Redis) - Fast, medium size
    ↓ (miss)
Database - Slowest, largest

When to Use Multi-Tier

  • High-performance requirements: Sub-millisecond response needed
  • Cost optimization: Reduce database load while controlling memory usage
  • Distributed systems: Shared cache across multiple instances
  • Hot data patterns: Small subset of data accessed frequently

Implementation

// src/app.module.ts
import { HazelModule } from '@hazeljs/core';
import { CacheModule } from '@hazeljs/cache';

@HazelModule({
  imports: [
    CacheModule.forRoot({
      strategy: 'multi-tier',
      redis: {
        host: process.env.REDIS_HOST,
        port: parseInt(process.env.REDIS_PORT),
      },
      memory: {
        maxSize: 1000, // Maximum entries in L-1 cache
        ttl: 300, // 5 minutes for L-1
      },
      redis: {
        ttl: 3600, // 1 hour for L-2
      },
      // L-1 gets 5 minutes, L-2 gets 1 hour
      lOneTTL: 300,
      lTwoTTL: 3600,
    }),
  ],
})
export class AppModule {}

Multi-Tier Service Implementation

// src/services/product.service.ts
@Service()
export class ProductService extends BaseService {
  // Hot products - cached in L-1 for instant access
  @Cache({
    key: 'hot-product-{id}',
    ttl: 300, // 5 minutes (L-1)
    tags: ['hot-products']
  })
  async getHotProduct(id: string) {
    return await this.db.product.findUnique({ 
      where: { id, isHot: true } 
    });
  }

  // Regular products - cached in L-2
  @Cache({
    key: 'product-{id}',
    ttl: 3600, // 1 hour (L-2)
    tags: ['products']
  })
  async getProduct(id: string) {
    return await this.db.product.findUnique({ where: { id } });
  }

  // Search results - medium cache duration
  @Cache({
    key: 'search-{query}-{page}',
    ttl: 1800, // 30 minutes
    tags: ['search-results']
  })
  async searchProducts(query: string, page: number = 1) {
    return await this.db.product.findMany({
      where: {
        OR: [
          { name: { contains: query } },
          { description: { contains: query } },
          { category: { contains: query } }
        ]
      },
      skip: (page - 1) * 20,
      take: 20
    });
  }

  // Analytics - longer cache, L-2 only
  @Cache({
    key: 'product-analytics-{id}',
    ttl: 7200, // 2 hours
    tags: ['analytics']
  })
  async getProductAnalytics(id: string) {
    const [views, purchases, ratings] = await Promise.all([
      this.db.view.count({ where: { productId: id } }),
      this.db.purchase.count({ where: { productId: id } }),
      this.db.rating.aggregate({ 
        where: { productId: id },
        _avg: { score: true }
      })
    ]);

    return { views, purchases, avgRating: ratings._avg.score };
  }
}

Tier-Specific Operations

// src/services/cache-admin.service.ts
@Service()
export class CacheAdminService extends BaseService {
  // Clear specific tier
  async clearTier(tier: 'l-1' | 'l-2' | 'all') {
    switch (tier) {
      case 'l-1':
        await this.cache.clearLOne();
        break;
      case 'l-2':
        await this.cache.clearLTwo();
        break;
      case 'all':
        await this.cache.clear();
        break;
    }
    return { cleared: tier };
  }

  // Get tier-specific statistics
  async getTierStats() {
    const lOneStats = await this.cache.getLOneStats();
    const lTwoStats = await this.cache.getLTwoStats();
    
    return {
      'l-1': {
        size: lOneStats.size,
        hitRate: lOneStats.hitRate,
        memoryUsage: lOneStats.memoryUsage
      },
      'l-2': {
        size: lTwoStats.size,
        hitRate: lTwoStats.hitRate,
        memoryUsage: lTwoStats.memoryUsage
      },
      combined: {
        totalHitRate: this.calculateCombinedHitRate(lOneStats, lTwoStats)
      }
    };
  }

  // Promote data from L-2 to L-1
  async promoteToLOne(key: string) {
    const value = await this.cache.getFromLTwo(key);
    if (value) {
      await this.cache.setToLOne(key, value, 300); // 5 minutes
      return { promoted: key };
    }
    return { promoted: null, reason: 'not_found_in_l_two' };
  }

  private calculateCombinedHitRate(lOne: any, lTwo: any): number {
    const totalRequests = lOne.hits + lOne.misses + lTwo.hits + lTwo.misses;
    const totalHits = lOne.hits + lTwo.hits;
    return totalRequests > 0 ? (totalHits / totalRequests) * 100 : 0;
  }
}

Performance Optimization

Optimizing cache performance involves monitoring, tuning, and implementing best practices.

Key Performance Metrics

// src/services/cache-metrics.service.ts
@Service()
export class CacheMetricsService extends BaseService {
  async getPerformanceReport() {
    const stats = await this.cache.getStats();
    const health = await this.cache.getHealth();
    
    return {
      performance: {
        hitRate: stats.hitRate,
        avgLatency: health.latency,
        errorRate: health.errorRate,
        throughput: this.calculateThroughput(stats)
      },
      memory: {
        usage: health.memoryUsage,
        efficiency: this.calculateMemoryEfficiency(stats),
        fragmentation: this.calculateFragmentation(health)
      },
      recommendations: this.generateRecommendations(stats, health)
    };
  }

  private calculateThroughput(stats: CacheStats): number {
    // Requests per second
    const timeWindow = 60; // 1 minute
    return (stats.hits + stats.misses) / timeWindow;
  }

  private calculateMemoryEfficiency(stats: CacheStats): number {
    // Bytes per entry
    return stats.memoryUsage ? stats.memoryUsage / stats.size : 0;
  }

  private calculateFragmentation(health: CacheHealth): number {
    // Estimate fragmentation based on memory patterns
    return health.memoryUsage * 0.1; // Simplified calculation
  }

  private generateRecommendations(stats: CacheStats, health: CacheHealth): string[] {
    const recommendations: string[] = [];
    
    if (stats.hitRate < 70) {
      recommendations.push('Consider increasing cache TTL or warming strategies');
    }
    
    if (health.latency > 10) {
      recommendations.push('Cache latency is high, consider memory-only cache for hot data');
    }
    
    if (health.errorRate > 0.01) {
      recommendations.push('High error rate detected, check cache connectivity');
    }
    
    if (stats.size > 10000) {
      recommendations.push('Large cache size, consider implementing eviction policies');
    }
    
    return recommendations;
  }
}

Performance Tuning Strategies

1. TTL Optimization

// Dynamic TTL based on access patterns
@Service()
export class SmartCacheService extends BaseService {
  @Cache({
    key: 'user-{id}',
    ttl: async function(this: SmartCacheService, id: string) {
      // Dynamic TTL based on user activity
      const user = await this.getUserActivity(id);
      
      if (user.isActive) {
        return 1800; // 30 minutes for active users
      } else if (user.recentlyActive) {
        return 7200; // 2 hours for recently active
      } else {
        return 86400; // 24 hours for inactive users
      }
    }
  })
  async getUser(id: string) {
    return await this.db.user.findUnique({ where: { id } });
  }

  private async getUserActivity(id: string) {
    const lastLogin = await this.db.user.findUnique({
      where: { id },
      select: { lastLoginAt: true }
    });
    
    const now = new Date();
    const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
    const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
    
    return {
      isActive: lastLogin?.lastLoginAt > oneDayAgo,
      recentlyActive: lastLogin?.lastLoginAt > oneWeekAgo
    };
  }
}

2. Cache Partitioning

// Partition cache by data type and access pattern
@Service()
export class PartitionedCacheService extends BaseService {
  // Hot data - small TTL, high priority
  @Cache({
    key: 'hot-{type}-{id}',
    ttl: 300,
    tags: ['hot-data']
  })
  async getHotData(type: string, id: string) {
    return await this.getData(type, id);
  }

  // Warm data - medium TTL
  @Cache({
    key: 'warm-{type}-{id}',
    ttl: 1800,
    tags: ['warm-data']
  })
  async getWarmData(type: string, id: string) {
    return await this.getData(type, id);
  }

  // Cold data - long TTL, low priority
  @Cache({
    key: 'cold-{type}-{id}',
    ttl: 86400,
    tags: ['cold-data']
  })
  async getColdData(type: string, id: string) {
    return await this.getData(type, id);
  }

  private async getData(type: string, id: string) {
    // Determine data temperature and route to appropriate cache
    const accessStats = await this.getAccessStats(type, id);
    
    if (accessStats.accessCount > 100) {
      return await this.getHotData(type, id);
    } else if (accessStats.accessCount > 10) {
      return await this.getWarmData(type, id);
    } else {
      return await this.getColdData(type, id);
    }
  }
}

Monitoring and Debugging

Effective monitoring helps you understand cache performance and identify issues quickly.

Cache Monitoring Dashboard

// src/services/cache-monitor.service.ts
@Service()
export class CacheMonitorService extends BaseService {
  async getDashboardData() {
    const [stats, health, recentEvents] = await Promise.all([
      this.cache.getStats(),
      this.cache.getHealth(),
      this.getRecentEvents()
    ]);

    return {
      overview: {
        totalEntries: stats.size,
        hitRate: stats.hitRate,
        errorRate: health.errorRate,
        latency: health.latency,
        memoryUsage: health.memoryUsage
      },
      performance: {
        hits: stats.hits,
        misses: stats.misses,
        evictions: stats.evictions || 0,
        throughput: this.calculateThroughput(stats)
      },
      health: {
        status: health.status,
        uptime: health.uptime,
        lastCheck: health.lastCheck
      },
      events: recentEvents.slice(0, 50), // Last 50 events
      alerts: this.generateAlerts(stats, health)
    };
  }

  private async getRecentEvents() {
    // Get recent cache events for debugging
    return await this.cache.getEvents({
      limit: 100,
      types: ['hit', 'miss', 'eviction', 'error']
    });
  }

  private generateAlerts(stats: CacheStats, health: CacheHealth) {
    const alerts = [];

    if (stats.hitRate < 50) {
      alerts.push({
        level: 'warning',
        message: 'Low cache hit rate detected',
        value: `${stats.hitRate}%`
      });
    }

    if (health.errorRate > 0.05) {
      alerts.push({
        level: 'error',
        message: 'High cache error rate',
        value: `${(health.errorRate * 100).toFixed(2)}%`
      });
    }

    if (health.latency > 100) {
      alerts.push({
        level: 'warning',
        message: 'High cache latency',
        value: `${health.latency}ms`
      });
    }

    return alerts;
  }
}

Debugging Tools

// src/services/cache-debug.service.ts
@Service()
export class CacheDebugService extends BaseService {
  async traceKey(key: string) {
    const entry = await this.cache.getEntry(key);
    const metadata = await this.cache.getMetadata(key);
    
    return {
      key,
      exists: !!entry,
      entry: entry ? {
        value: entry.value,
        cachedAt: entry.cachedAt,
        expiresAt: entry.expiresAt,
        lastAccessedAt: entry.lastAccessedAt,
        tags: entry.tags
      } : null,
      metadata,
      recommendations: this.analyzeEntry(entry, metadata)
    };
  }

  async analyzePattern(pattern: string) {
    const keys = await this.cache.keys(pattern);
    const analysis = await Promise.all(
      keys.map(async key => this.traceKey(key))
    );

    return {
      pattern,
      totalKeys: keys.length,
      totalSize: analysis.reduce((sum, item) => 
        sum + (item.entry ? JSON.stringify(item.entry.value).length : 0), 0
      ),
      avgTTL: this.calculateAverageTTL(analysis),
      hotKeys: this.findHotKeys(analysis),
      staleKeys: this.findStaleKeys(analysis),
      recommendations: this.generatePatternRecommendations(analysis)
    };
  }

  private analyzeEntry(entry: any, metadata: any) {
    const recommendations = [];

    if (!entry) {
      recommendations.push('Key not found in cache');
      return recommendations;
    }

    const now = Date.now();
    const age = now - entry.cachedAt;
    const ttl = entry.expiresAt - entry.cachedAt;

    if (age > ttl * 0.8) {
      recommendations.push('Entry is near expiration, consider refreshing');
    }

    if (!entry.lastAccessedAt || (now - entry.lastAccessedAt) > ttl * 0.5) {
      recommendations.push('Entry hasn't been accessed recently, consider longer TTL');
    }

    if (metadata?.accessCount < 5) {
      recommendations.push('Low access count, consider if caching is necessary');
    }

    return recommendations;
  }

  private findHotKeys(analysis: any[]) {
    return analysis
      .filter(item => item.metadata?.accessCount > 50)
      .map(item => ({
        key: item.key,
        accessCount: item.metadata.accessCount,
        hitRate: item.metadata.hitRate
      }));
  }

  private findStaleKeys(analysis: any[]) {
    const now = Date.now();
    return analysis
      .filter(item => {
        if (!item.entry) return false;
        const age = now - item.entry.cachedAt;
        const ttl = item.entry.expiresAt - item.entry.cachedAt;
        return age > ttl * 0.9;
      })
      .map(item => item.key);
  }
}

Common Pitfalls and Solutions

1. Cache Stampede

Problem: Multiple requests simultaneously computing the same expensive value.

Solution: Use distributed locking.

@CacheLock({
  key: 'expensive-{param}',
  ttl: 30000
})
async expensiveOperation(param: string) {
  // Only one instance runs at a time
  return await computeExpensiveValue(param);
}

2. Stale Data

Problem: Cache contains outdated information.

Solution: Implement proper invalidation strategies.

@Service()
export class ProductService {
  @CacheEvict({ tags: ['products'] })
  async updateProduct(id: string, data: any) {
    const updated = await this.db.product.update({ where: { id }, data });
    // All product-related caches are invalidated
    return updated;
  }

  @CacheEvict({ keys: ['product-{id}'] })
  async deleteProduct(id: string) {
    return await this.db.product.delete({ where: { id } });
  }
}

3. Memory Leaks

Problem: Cache grows indefinitely and consumes all memory.

Solution: Set appropriate TTLs and implement cleanup.

CacheModule.forRoot({
  strategy: 'memory',
  ttl: 3600, // Default 1 hour
  cleanupInterval: 60000, // Cleanup every minute
  maxSize: 10000, // Maximum 10,000 entries
  evictionPolicy: 'lru' // Least recently used eviction
});

4. Cache Inconsistency

Problem: Cache and database are out of sync.

Solution: Use write-through for critical data.

@WriteThrough({
  key: 'user-{id}',
  ttl: 1800
})
async updateUser(id: string, data: any) {
  // Database and cache updated together
  return await this.db.user.update({ where: { id }, data });
}

5. Over-Caching

Problem: Caching data that shouldn't be cached.

Solution: Implement cache conditions and exclusions.

@Cache({
  key: 'data-{id}',
  ttl: 3600,
  condition: (value) => value !== null && value !== undefined,
  cacheNull: false
})
async getData(id: string) {
  const data = await this.db.data.findUnique({ where: { id } });
  return data; // Won't cache null/undefined values
}

Testing Cache Implementation

Unit Testing Strategies

// tests/services/cache.test.ts
describe('UserService Cache', () => {
  let service: UserService;
  let cacheService: CacheService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [UserService, CacheService],
    }).compile();

    service = module.get<UserService>(UserService);
    cacheService = module.get<CacheService>(CacheService);
  });

  describe('Cache-Aside Pattern', () => {
    it('should cache user data', async () => {
      const userId = '123';
      
      // First call - cache miss
      const userOne = await service.getUser(userId);
      expect(cacheService.get).toHaveBeenCalledWith(`user-${userId}`);
      
      // Second call - cache hit
      const userTwo = await service.getUser(userId);
      expect(userOne).toEqual(userTwo);
    });

    it('should return fallback when user not found', async () => {
      const userId = '999';
      
      const user = await service.getUser(userId);
      expect(user).toEqual({
        id: 'anonymous',
        name: 'Guest User',
        role: 'guest'
      });
    });
  });

  describe('Cache Invalidation', () => {
    it('should invalidate cache on update', async () => {
      const userId = '123';
      
      // Cache user
      await service.getUser(userId);
      
      // Update user
      await service.updateUser(userId, { name: 'New Name' });
      
      // Verify cache was invalidated
      expect(cacheService.invalidateTags).toHaveBeenCalledWith(['users']);
    });
  });
});

Integration Testing

// tests/integration/cache.integration.test.ts
describe('Cache Integration', () => {
  let app: HazelApplication;

  beforeAll(async () => {
    app = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
    
    await app.init();
  });

  it('should handle distributed locking', async () => {
    // Simulate concurrent requests
    const promises = Array.from({ length: 5 }, (_, i) => 
      fetch(`/api/reports/daily/${new Date().toISOString()}`)
    );
    
    const results = await Promise.all(promises);
    
    // Only one should have computed the report
    const computations = results.filter(r => r.headers.get('x-cache-computed') === 'true');
    expect(computations).toHaveLength(1);
  });

  it('should maintain cache consistency', async () => {
    const userId = '123';
    
    // Get user
    const getOne = await fetch(`/api/users/${userId}`);
    const userOne = await getOne.json();
    
    // Update user
    await fetch(`/api/users/${userId}`, {
      method: 'PUT',
      body: JSON.stringify({ name: 'Updated Name' })
    });
    
    // Get user again
    const getTwo = await fetch(`/api/users/${userId}`);
    const userTwo = await getTwo.json();
    
    expect(userTwo.name).toBe('Updated Name');
    expect(userTwo.name).not.toBe(userOne.name);
  });
});

Conclusion

Advanced caching strategies can dramatically improve your application's performance, but they require careful planning and implementation. Here are the key takeaways:

  • Choose the right pattern: Match caching strategy to your use case
  • Monitor continuously: Track hit rates, latency, and error rates
  • Plan for failures: Implement fallbacks and error handling
  • Test thoroughly: Verify cache behavior under various conditions
  • Optimize iteratively: Start simple and add complexity as needed

By following these patterns and best practices, you can build highly performant, scalable applications that make effective use of caching at every level.

Next Steps