DocumentationReference

HazelJS Cache Package

npm downloads

@hazeljs/cache provides caching for HazelJS applications with memory, Redis, and multi-tier strategies, decorator-based method caching, and tag-based invalidation.

Quick Reference

  • Purpose: @hazeljs/cache provides method-level caching with decorators, multiple storage backends (memory, Redis, multi-tier), tag-based invalidation, and TTL management for HazelJS applications.
  • When to use: Use @hazeljs/cache to cache expensive computations, database queries, AI responses, or API calls. Especially useful for caching LLM responses to reduce latency and cost.
  • Key concepts: CacheModule.forRoot(), @Cacheable() method decorator, @CacheEvict() decorator, @CachePut() decorator, cache strategies (memory, Redis, multi-tier), tag-based invalidation, TTL (time-to-live), advanced decorators (@CacheLock, @CacheAside, @WriteThrough, @WriteBehind, @CacheWarm).
  • Dependencies: @hazeljs/core, optionally ioredis for Redis backend.
  • Common patterns: Register CacheModule.forRoot({ strategy: 'memory' }) → decorate service methods with @Cacheable({ key, ttl }) → use @CacheEvict({ tags }) on mutation methods to invalidate related entries → use advanced decorators for distributed locking (@CacheLock), cache-aside pattern (@CacheAside), write strategies (@WriteThrough, @WriteBehind), and cache warming (@CacheWarm).
  • Common mistakes: Not invalidating cache on mutations (stale data); using memory cache in multi-instance deployments (not shared); setting TTL too long for frequently changing data; caching user-specific data without including userId in the cache key.

Purpose

Caching is essential for building high-performance applications, but implementing it correctly can be complex. You need to handle cache invalidation, manage different storage backends, implement cache-aside patterns, and ensure data consistency. The @hazeljs/cache package solves these challenges by providing:

  • Multiple Strategies: Choose from in-memory, Redis, or multi-tier caching based on your needs
  • Decorator-Based Caching: Add caching to any method with a simple decorator
  • Tag-Based Invalidation: Invalidate related cache entries using tags instead of managing individual keys
  • Automatic TTL Management: Built-in time-to-live handling with flexible strategies
  • Cache-Aside Pattern: Built-in support for the cache-aside pattern with automatic fallback

Architecture

The package uses a strategy pattern that allows you to swap cache implementations without changing your code:

graph TD
  A["Your Application Code<br/>(@Cache decorators, CacheService)"] --> B["CacheService<br/>(Unified Cache Interface)"]
  B --> C["Strategy Abstraction<br/>(ICacheStore Interface)"]
  C --> D["Memory Cache"]
  C --> E["Redis Cache"]
  C --> F["Multi-Tier Cache"]
  
  style "A" fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style "B" fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style "C" fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style "D" fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style "E" fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style "F" fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff

Key Components

  • CacheService: Main service providing unified cache operations
  • Cache Strategies: Pluggable implementations (Memory, Redis, Multi-Tier)
  • Decorators: @Cache, @CacheKey, @CacheTTL, @CacheTags, @CacheEvict, and advanced decorators @CacheLock, @CacheAside, @WriteThrough, @WriteBehind, @CacheWarm
  • CacheManager: Manages multiple cache instances for different use cases

Advantages

1. Performance Optimization

Dramatically reduce database queries and API calls by caching frequently accessed data. Response times can improve by 10-100x for cached data.

2. Flexible Storage Options

Start with in-memory caching for development, switch to Redis for distributed systems, or use multi-tier for optimal performance and cost.

3. Developer-Friendly API

Decorator-based approach means you can add caching to any method with a single line. No need to manually implement cache logic.

4. Smart Invalidation

Tag-based invalidation allows you to invalidate related cache entries together. Update a user? Invalidate all user-related caches automatically.

5. Production Features

Includes cache warming, statistics tracking, automatic cleanup, and support for cache-aside patterns.

6. Type Safety

Full TypeScript support ensures type-safe cache operations and prevents runtime errors.

Installation

npm install @hazeljs/cache

Quick Start

Basic Setup

import { HazelModule } from '@hazeljs/core';
import { CacheModule } from '@hazeljs/cache';

@HazelModule({
  imports: [
    CacheModule.register({
      strategy: 'memory',
      ttl: 3600, // 1 hour default TTL
    }),
  ],
})
export class AppModule {}

Cache Service

Basic Usage

import { Service } from '@hazeljs/core';
import { CacheService } from '@hazeljs/cache';

@Service()
export class UserService {
  constructor(private readonly cache: CacheService) {}

  async getUser(id: string) {
    // Try to get from cache
    const cached = await this.cache.get<User>(`user:${id}`);
    if (cached) {
      return cached;
    }

    // Fetch from database
    const user = await this.fetchUserFromDb(id);

    // Store in cache
    await this.cache.set(`user:${id}`, user, 3600); // 1 hour TTL

    return user;
  }
}

Cache-Aside Pattern

Use the getOrSet method for the cache-aside pattern:

@Service()
export class ProductService {
  constructor(private readonly cache: CacheService) {}

  async getProduct(id: string) {
    return await this.cache.getOrSet(
      `product:${id}`,
      async () => {
        // This function is only called if cache miss
        return await this.fetchProductFromDb(id);
      },
      3600, // TTL
      ['products', 'product-list'] // Tags for invalidation
    );
  }
}

Cache Decorators

The cache package provides a comprehensive set of decorators for managing caching behavior. These decorators use metadata to configure caching at the method level, making it easy to add caching without modifying your business logic.

Understanding Cache Decorators

Cache decorators are method decorators that store caching configuration in metadata. When a method is called, the framework intercepts it, checks the cache, and either returns cached data or executes the method and caches the result.

How Cache Decorators Work:

  • Metadata Storage: Decorators store cache configuration using reflection
  • Method Interception: The framework wraps your method with caching logic
  • Cache Lookup: Before execution, checks if cached data exists
  • Result Caching: After execution, stores the result in cache
  • Key Generation: Automatically generates cache keys from method parameters

@Cache Decorator

The @Cache decorator is the primary decorator for enabling caching on a method. It configures how the method's results should be cached.

Purpose: Automatically cache method return values with configurable TTL, key patterns, and tags.

How it works:

  • Intercepts method calls before execution
  • Generates a cache key from the method name and parameters
  • Checks cache for existing value
  • If cache hit: returns cached value (method doesn't execute)
  • If cache miss: executes method, caches result, returns value

Configuration Options:

interface CacheOptions {
  ttl?: number;              // Time-to-live in seconds (default: 3600)
  key?: string;              // Custom key pattern with {param} placeholders
  tags?: string[];           // Tags for bulk invalidation
  strategy?: string;         // Cache strategy: 'memory', 'redis', 'multi-tier'
  ttlStrategy?: string;      // 'absolute' or 'sliding' TTL
  cacheNull?: boolean;       // Cache null/undefined values (default: false)
}

Example with Detailed Explanation:

import { Controller, Get, Param } from '@hazeljs/core';
import { Cache } from '@hazeljs/cache';

@Controller('users')
export class UsersController {
  @Get(':id')
  // @Cache is a method decorator that enables caching
  @Cache({
    ttl: 3600,                    // Cache for 1 hour (3600 seconds)
    key: 'user-{id}',             // Custom key pattern
    // {id} will be replaced with the actual id parameter value
    // Resulting key: 'user-123' for id='123'
    tags: ['users'],               // Tag for bulk invalidation
    // When you invalidate 'users' tag, all entries with this tag are cleared
  })
  async getUser(@Param('id') id: string) {
    // First call: Method executes, result cached with key 'user-123'
    // Subsequent calls with same id: Returns from cache, method doesn't execute
    return await this.userService.findOne(id);
  }
}

Key Pattern Placeholders:

  • {paramName} - Replaced with the value of the parameter named paramName
  • {0}, {1} - Replaced with parameter at index 0, 1, etc.
  • Method name and class name are available as {method} and {class}

@CacheKey Decorator

Purpose: Specifies a custom cache key generation pattern. Use this when you need fine-grained control over cache keys.

How it works:

  • Overrides the default key generation
  • Supports parameter placeholders
  • Can combine multiple parameters in the key

When to use:

  • When default key generation doesn't meet your needs
  • When you need to include query parameters in the key
  • When you want to share cache across different methods

Example with Detailed Explanation:

@Get(':id')
// @CacheKey decorator specifies the key pattern
@CacheKey('user-{id}-{role}')
// This creates keys like: 'user-123-admin' or 'user-123-user'
@Cache({ ttl: 3600 })
async getUser(
  @Param('id') id: string,        // Used in key as {id}
  @Query('role') role: string     // Used in key as {role}
) {
  // Cache key will be: 'user-{id}-{role}'
  // Example: getUser('123', 'admin') → key: 'user-123-admin'
  // Different role values create different cache entries
  return await this.userService.findOne(id);
}

Key Generation Rules:

  • Placeholders are replaced with actual parameter values
  • If a parameter is undefined, it's replaced with 'undefined'
  • Objects are stringified (consider using specific properties instead)
  • Keys are case-sensitive

@CacheTTL Decorator

Purpose: Sets the time-to-live (TTL) for cached entries. This decorator is a convenience method for setting TTL without using the full @Cache options.

How it works:

  • Sets the TTL value in cache metadata
  • Can be combined with other cache decorators
  • Overrides TTL from @Cache if both are present

Example with Detailed Explanation:

@Get('popular')
// @CacheTTL is a method decorator that sets cache expiration
@CacheTTL(7200) // Cache for 2 hours (7200 seconds)
// This is equivalent to @Cache({ ttl: 7200 })
@Cache() // Still need @Cache to enable caching
async getPopularProducts() {
  // Results cached for 2 hours
  // After 2 hours, cache expires and method executes again
  return await this.productService.findPopular();
}

TTL Strategies:

  • Absolute TTL: Cache expires at a fixed time from creation
  • Sliding TTL: Cache expiration extends on each access (configured in @Cache)

@CacheTags Decorator

Purpose: Assigns tags to cache entries for bulk invalidation. Tags allow you to invalidate related cache entries together.

How it works:

  • Stores tags in cache metadata
  • When you invalidate a tag, all entries with that tag are cleared
  • Supports multiple tags per method

When to use:

  • When you need to invalidate related data together
  • When data relationships change (e.g., user update affects user list)
  • For cache warming strategies

Example with Detailed Explanation:

@Get(':id')
// @CacheTags assigns tags to this cache entry
@CacheTags(['users', 'profiles'])
// Both 'users' and 'profiles' tags are assigned
@Cache({ ttl: 3600 })
async getUserProfile(@Param('id') id: string) {
  // This entry is tagged with both 'users' and 'profiles'
  // Invalidating either tag will clear this cache entry
  return await this.userService.getProfile(id);
}

// Later, when user data changes:
@Put(':id')
@CacheEvict({ tags: ['users'] }) // Invalidates all 'users' tagged entries
async updateUser(@Param('id') id: string, @Body() data: UpdateUserDto) {
  // This will clear getUserProfile cache (and any other 'users' tagged entries)
  return await this.userService.update(id, data);
}

Tag Best Practices:

  • Use descriptive tag names: 'users', 'products', 'orders'
  • Group related data with the same tags
  • Use hierarchical tags: 'users:profiles', 'users:settings'
  • Don't over-tag: Too many tags make invalidation inefficient

@CacheEvict Decorator

Purpose: Evicts (removes) cache entries when a method executes. Used for cache invalidation when data changes.

How it works:

  • Executes before or after the method (configurable)
  • Removes cache entries matching keys or tags
  • Supports evicting all cache entries

Configuration Options:

interface CacheEvictOptions {
  keys?: string[];           // Specific keys to evict (supports patterns)
  tags?: string[];           // Tags to evict (removes all entries with these tags)
  all?: boolean;             // Evict all cache entries (use with caution)
  beforeInvocation?: boolean; // Evict before method execution (default: false)
}

Example with Detailed Explanation:

// Create operation - invalidate related caches
@Post()
// @CacheEvict removes cache entries when this method executes
@CacheEvict({ 
  tags: ['users']  // Remove all cache entries tagged with 'users'
  // This includes: user lists, user profiles, user stats, etc.
})
async createUser(@Body() createUserDto: CreateUserDto) {
  // Before or after creating user, all 'users' tagged entries are cleared
  // Next call to getUser() or getUsers() will execute and cache fresh data
  return await this.userService.create(createUserDto);
}

// Update operation - evict specific and related caches
@Put(':id')
@CacheEvict({ 
  keys: ['user-{id}'],  // Remove specific user cache
  tags: ['users']       // Also remove all 'users' tagged entries
  // This ensures both the specific user and user lists are refreshed
})
async updateUser(
  @Param('id') id: string,
  @Body() updateUserDto: UpdateUserDto
) {
  // Evicts: 'user-123' cache AND all 'users' tagged entries
  return await this.userService.update(id, updateUserDto);
}

// Delete operation - comprehensive eviction
@Delete(':id')
@CacheEvict({ 
  keys: ['user-{id}'],  // Remove specific user
  tags: ['users'],       // Remove all user-related caches
  all: true              // Also clear entire cache (optional, use carefully)
})
async deleteUser(@Param('id') id: string) {
  // Most aggressive eviction - clears specific key, tags, and optionally all cache
  return await this.userService.delete(id);
}

Eviction Timing:

  • After execution (default): Evicts after method completes successfully
  • Before execution: Set beforeInvocation: true to evict before method runs
  • Use before eviction when you want to ensure fresh data is always fetched

Combining Decorators:

You can combine multiple cache decorators for fine-grained control:

@Get(':id')
@CacheKey('user-{id}')           // Custom key pattern
@CacheTTL(7200)                  // 2 hour TTL
@CacheTags(['users', 'profiles']) // Multiple tags
@Cache()                          // Enable caching
async getUser(@Param('id') id: string) {
  // All decorators work together:
  // - Key: 'user-{id}'
  // - TTL: 7200 seconds
  // - Tags: ['users', 'profiles']
  return await this.userService.findOne(id);
}

Decorator Execution Order:

  • @CacheEvict (if beforeInvocation: true)
  • Cache lookup (if @Cache is present)
  • Method execution (if cache miss)
  • Result caching (if @Cache is present)
  • @CacheEvict (if beforeInvocation: false)

Advanced Cache Decorators

The cache package provides advanced decorators for sophisticated caching patterns beyond basic method caching. These decorators handle complex scenarios like distributed locking, cache-aside patterns, write strategies, and intelligent cache warming.

@CacheLock Decorator

Purpose: Prevents cache stampede by ensuring only one instance executes expensive operations while multiple requests wait for the result.

How it works:

  • Acquires distributed lock before method execution
  • First request executes method, others wait for lock
  • Result is cached and shared with all waiting requests
  • Prevents thundering herd problems

When to use:

  • Expensive computations (analytics, report generation)
  • Rate-limited API calls
  • Resource-intensive operations
  • When multiple users might trigger same expensive operation

Configuration Options:

interface CacheLockOptions {
  key: string;              // Lock key pattern
  ttl?: number;             // Lock TTL in milliseconds (default: 30000)
  retryDelay?: number;      // Retry delay in milliseconds (default: 1000)
  maxRetries?: number;      // Maximum retry attempts (default: 3)
}

Example with Detailed Explanation:

import { CacheLock } from '@hazeljs/cache';

@Service()
export class AnalyticsService {
  @CacheLock({
    key: 'analytics-report-{date}-{type}',
    ttl: 60000,              // 1 minute lock
    retryDelay: 2000,       // Check every 2 seconds
    maxRetries: 5           // Try up to 5 times
  })
  async generateReport(date: string, type: string) {
    console.log(`🔒 Generating report for ${date} - ${type}`);
    
    // Expensive computation that should only run once
    await new Promise(resolve => setTimeout(resolve, 5000));
    
    const report = {
      date,
      type,
      data: `Complex analytics for ${date}`,
      generatedAt: new Date().toISOString()
    };
    
    return report;
  }
}

// Usage:
// Request 1: Acquires lock, executes method (5 seconds)
// Request 2-5: Wait for lock, get result from first request
// Request 6+: Get result from cache (if cached with @Cache)

Lock Behavior:

  • Lock Acquisition: Uses Redis or memory-based distributed locking
  • Lock Timeout: Locks auto-expire to prevent deadlocks
  • Retry Strategy: Exponential backoff with configurable delays
  • Error Handling: Failed lock attempts throw CacheLockError

@CacheAside Decorator

Purpose: Implements the cache-aside pattern automatically with get/set logic and optional fallback values.

How it works:

  • Checks cache before method execution
  • Executes method only on cache miss
  • Automatically caches successful results
  • Returns fallback value on cache miss and method failure

When to use:

  • Database queries with cache-aside pattern
  • API calls with automatic caching
  • When you want automatic get/set logic
  • When you need fallback values for resilience

Configuration Options:

interface CacheAsideOptions {
  key: string;              // Cache key pattern
  ttl?: number;             // TTL in seconds (default: 3600)
  fallback?: () => Promise<unknown>; // Fallback function
}

Example with Detailed Explanation:

import { CacheAside, CacheAsideWithFallback } from '@hazeljs/cache';

@Service()
export class ProductService {
  // Basic cache-aside
  @CacheAside({
    key: 'product-{id}',
    ttl: 3600,              // 1 hour cache
  })
  async getProduct(id: string) {
    console.log(`📥 Fetching product ${id} from database`);
    return await this.db.product.findUnique({ where: { id } });
  }

  // Cache-aside with fallback
  @CacheAsideWithFallback({
    key: 'user-{id}',
    ttl: 1800,              // 30 minutes
    fallbackValue: { 
      id: 'unknown', 
      name: 'Guest User', 
      role: 'guest' 
    }
  })
  async getUser(id: string) {
    console.log(`👤 Fetching user ${id} from database`);
    return await this.db.user.findUnique({ where: { id } });
  }

  // Cache-aside with dynamic fallback
  @CacheAside({
    key: 'config-{key}',
    ttl: 7200,              // 2 hours
    fallback: async function(this: ProductService, key: string) {
      // Dynamic fallback based on key
      return await this.getDefaultConfig(key);
    }
  })
  async getConfig(key: string) {
    return await this.db.config.findUnique({ where: { key } });
  }
}

Cache-Aside Flow:

  • Cache Check: Look for value in cache first
  • Cache Hit: Return cached value immediately
  • Cache Miss: Execute original method
  • Cache Store: Cache successful results
  • Fallback: Return fallback if method fails

@WriteThrough Decorator

Purpose: Updates cache immediately when data changes, ensuring cache and database stay synchronized.

How it works:

  • Executes method (database operation)
  • On success, immediately updates cache
  • Ensures cache reflects latest data
  • Provides strong consistency

When to use:

  • Critical data that must always be current
  • When cache consistency is more important than performance
  • Update operations that affect cached data
  • When you need immediate cache updates

Configuration Options:

interface CacheWriteOptions {
  key: string;              // Cache key pattern
  ttl?: number;             // TTL in seconds
  strategy?: 'write-through' | 'write-behind'; // Write strategy
}

Example with Detailed Explanation:

import { WriteThrough } from '@hazeljs/cache';

@Service()
export class UserService {
  @WriteThrough({
    key: 'user-{id}',
    ttl: 3600,              // 1 hour cache
  })
  async updateUser(id: string, data: UpdateUserDto) {
    console.log(`✏️ Updating user ${id} in database and cache`);
    
    // 1. Update database
    const updatedUser = await this.db.user.update({
      where: { id },
      data
    });
    
    // 2. Decorator automatically updates cache
    // Cache key: 'user-{id}' will be set with updatedUser
    
    return updatedUser;
  }

  @WriteThrough({
    key: 'user-profile-{id}',
    ttl: 1800,              // 30 minutes
  })
  async updateProfile(id: string, profileData: ProfileDto) {
    // Update user profile
    const updated = await this.db.user.update({
      where: { id },
      data: { profile: profileData }
    });
    
    // Cache is automatically updated with new profile data
    return updated;
  }
}

Write-Through Benefits:

  • Strong Consistency: Cache always reflects database state
  • Immediate Updates: No delay between database and cache updates
  • Data Integrity: Reduces risk of stale data
  • Simplicity: No need for manual cache updates

@WriteBehind Decorator

Purpose: Queues cache updates for better performance, updating cache asynchronously after database operations succeed.

How it works:

  • Executes method (database operation)
  • Queues cache update for async processing
  • Improves write performance
  • Provides eventual consistency

When to use:

  • High-volume write operations
  • When write performance is critical
  • Non-critical data that can be eventually consistent
  • Batch operations

Configuration Options:

interface CacheWriteOptions {
  key: string;              // Cache key pattern
  ttl?: number;             // TTL in seconds
  strategy?: 'write-through' | 'write-behind'; // Write strategy
  async?: boolean;          // Async processing (default: true for write-behind)
}

Example with Detailed Explanation:

import { WriteBehind } from '@hazeljs/cache';

@Service()
export class AnalyticsService {
  @WriteBehind({
    key: 'analytics-{id}',
    ttl: 1800,              // 30 minutes
    async: true,            // Process cache updates asynchronously
  })
  async updateAnalytics(id: string, data: AnalyticsData) {
    console.log(`⏱️ Updating analytics ${id} in database, cache update queued`);
    
    // 1. Update database immediately
    const updated = await this.db.analytics.update({
      where: { id },
      data
    });
    
    // 2. Cache update is queued for async processing
    // Method returns immediately, cache updates in background
    
    return updated;
  }

  @WriteBehind({
    key: 'counter-{type}',
    ttl: 300,               // 5 minutes
    async: true,
  })
  async incrementCounter(type: string) {
    // High-frequency counter updates
    const updated = await this.db.counter.upsert({
      where: { type },
      update: { count: { increment: 1 } },
      create: { type, count: 1 }
    });
    
    // Cache update happens asynchronously
    return updated;
  }
}

Write-Behind Benefits:

  • Performance: Faster write operations
  • Throughput: Handles high-volume writes
  • Resource Efficiency: Reduces immediate cache load
  • Scalability: Better for write-heavy applications

@CacheWarm Decorator

Purpose: Pre-populates cache with frequently accessed data using scheduled or manual warming strategies.

How it works:

  • Defines warming jobs with keys and fetchers
  • Supports cron-based scheduling
  • Can warm based on conditions (e.g., low traffic)
  • Provides utilities for manual warming

When to use:

  • Frequently accessed data
  • Expensive computations
  • Scheduled reports
  • Performance-critical applications

Configuration Options:

interface CacheWarmOptions {
  keys: string[];           // Keys to warm up
  fetcher: (key: string) => Promise<unknown>; // Data fetcher
  ttl?: number;             // TTL for warmed entries
  parallel?: boolean;       // Parallel warming (default: true)
  schedule?: string;         // Cron schedule
  condition?: string;       // Warming condition
}

Example with Detailed Explanation:

import { CacheWarm, CacheWarmingUtils } from '@hazeljs/cache';

@Service()
export class ReportService {
  @CacheWarm({
    keys: [
      'daily-report-{date}',
      'weekly-report-{date}',
      'monthly-report-{date}'
    ],
    fetcher: async function(this: ReportService, key: string) {
      console.log(`🔥 Warming cache key: ${key}`);
      
      // Parse key to determine report type and date
      const [type, dateStr] = key.split('-');
      
      if (type === 'daily') {
        return await this.generateDailyReport(dateStr);
      } else if (type === 'weekly') {
        return await this.generateWeeklyReport(dateStr);
      } else if (type === 'monthly') {
        return await this.generateMonthlyReport(dateStr);
      }
    },
    ttl: 7200,               // 2 hours
    parallel: true,          // Warm all keys in parallel
    schedule: '0 2 * * *',   // Every day at 2 AM
    condition: 'low-traffic' // Only during low traffic
  })
  async warmReports() {
    console.log('🌡️ Cache warming method executed');
    return { message: 'Cache warming completed' };
  }

  // Manual warming trigger
  async triggerManualWarming() {
    const jobs = CacheWarmingUtils.listJobs();
    if (jobs.length > 0) {
      await CacheWarmingUtils.warmUp(jobs[0]);
      return { success: true, message: 'Manual warming completed' };
    }
    return { success: false, message: 'No warming jobs found' };
  }
}

Cache Warming Strategies:

  • Scheduled Warming: Cron-based automatic warming
  • Conditional Warming: Based on traffic patterns
  • Manual Warming: On-demand warming via utilities
  • Parallel Warming: Multiple keys simultaneously

Cache Warming Utilities:

// List all warming jobs
const jobs = CacheWarmingUtils.listJobs();

// Manually trigger warming for a specific job
await CacheWarmingUtils.warmUp(job);

// Get warming statistics
const stats = CacheWarmingUtils.getStats();

// Clean up warming jobs
CacheWarmingUtils.destroy();

Cache Architecture Patterns

Cache-Aside Pattern Flow

sequenceDiagram
  participant Client
  participant App as Application
  participant Cache
  participant DB as Database
  
  Client->>App: Request Data
  App->>Cache: get(key)
  alt Cache Hit
      Cache-->>App: Return Data
      App-->>Client: Response (from cache)
  else Cache Miss
      App->>DB: Query Data
      DB-->>App: Return Data
      App->>Cache: set(key, data, ttl)
      App-->>Client: Response (from DB)
  end
  
  Note over App: @CacheAside decorator handles this flow automatically

Write-Through vs Write-Behind

graph TD
  subgraph "Write-Through Pattern"
      "A-1"["Client Request"] --> "B-1"["Update Database"]
      "B-1" --> "C-1"["Update Cache Immediately"]
      "C-1" --> "D-1"["Response to Client"]
      style "B-1" fill:#ff9999,stroke:#ff6666
      style "C-1" fill:#ff9999,stroke:#ff6666
  end
  
  subgraph "Write-Behind Pattern"
      "A-2"["Client Request"] --> "B-2"["Update Database"]
      "B-2" --> "C-2"["Queue Cache Update"]
      "C-2" --> "D-2"["Response to Client"]
      "C-2" --> "E-2"["Async Cache Update"]
      style "B-2" fill:#99ff99,stroke:#66ff66
      style "C-2" fill:#99ff99,stroke:#66ff66
      style "E-2" fill:#ffff99,stroke:#cccc66
  end
  
  style "A-1" fill:#e1f5fe
  style "A-2" fill:#e1f5fe
  style "D-1" fill:#e8f5e8
  style "D-2" fill:#e8f5e8

Distributed Locking Pattern

sequenceDiagram
  participant "R1" as Request 1
  participant "R2" as Request 2
  participant "R3" as Request 3
  participant Lock as Cache Lock
  participant Cache
  participant DB as Database
  
  "R1"->>Lock: acquire(key)
  Lock-->>"R1": lock granted
  
  par Request 1 executes expensive operation
      "R1"->>DB: perform expensive query
      DB-->>"R1": result
      "R1"->>Cache: set(key, result)
      "R1"->>Lock: release(key)
  and Requests 2-3 wait for lock
      "R2"->>Lock: acquire(key)
      Lock-->>"R2": wait/retry
      "R3"->>Lock: acquire(key)
      Lock-->>"R3": wait/retry
  end
  
  "R2"->>Lock: acquire(key)
  Lock-->>"R2": lock granted (after release)
  Cache-->>"R2": return cached result
  "R3"->>Cache: get(key)
  Cache-->>"R3": return cached result
  
  Note over "R1","R3": Only one request executes, others get cached result

Multi-Tier Cache Architecture

graph TD
  "A"["Application"] --> "B"["L-1 Cache - Memory"]
  "B" -->|L-1 Miss| "C"["L-2 Cache - Redis"]
  "C" -->|L-2 Miss| "D"["Database"]
  
  "B" -.->|L-1 Hit| "A"
  "C" -.->|L-2 Hit| "A"
  
  subgraph "Cache Characteristics"
      "L-1"["L-1 Cache<br/>- Fastest (0.1ms)<br/>- Limited size<br/>- Per instance"]
      "L-2"["L-2 Cache<br/>- Fast (1ms)<br/>- Medium size<br/>- Shared"]
      "DB"["Database<br/>- Slow (10-100ms)<br/>- Large size<br/>- Persistent"]
  end
  
  style "A" fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style "B" fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style "C" fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style "D" fill:#f59e0b,stroke:#fbbf24,stroke-width:2px,color:#fff
  style "L-1" fill:#e0f2fe
  style "L-2" fill:#e0f2fe
  style "DB" fill:#fef3c7

Cache Warming Strategy

graph TD
  "A"["Schedule Trigger<br/>Cron: 0 2 * * *"] --> "B"{"Traffic Condition?"}
  "B" -->|Low Traffic| "C"["Start Warming Job"]
  "B" -->|High Traffic| "D"["Skip Warming"]
  
  "C" --> "E"["Generate Key List"]
  "E" --> "F"["Parallel Processing"]
  
  "F" --> "G"["Key 1: Fetch Data"]
  "F" --> "H"["Key 2: Fetch Data"]
  "F" --> "I"["Key 3: Fetch Data"]
  
  "G" --> "J"["Cache Key 1"]
  "H" --> "K"["Cache Key 2"]
  "I" --> "L"["Cache Key 3"]
  
  "J" --> "M"["Warming Complete"]
  "K" --> "M"
  "L" --> "M"
  
  "M" --> "N"["Update Statistics"]
  
  style "A" fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style "C" fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style "M" fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style "D" fill:#ef4444,stroke:#f87171,stroke-width:2px,color:#fff

Cache Strategies

Memory Cache

Default strategy, stores data in application memory:

CacheModule.register({
  strategy: 'memory',
  ttl: 3600,
  cleanupInterval: 60000, // Cleanup every minute
})

Redis Cache

Use Redis for distributed caching:

CacheModule.register({
  strategy: 'redis',
  redis: {
    host: 'localhost',
    port: 6379,
    password: process.env.REDIS_PASSWORD,
  },
  ttl: 3600,
})

Multi-Tier Cache

Combine memory and Redis for optimal performance:

CacheModule.register({
  strategy: 'multi-tier',
  redis: {
    host: 'localhost',
    port: 6379,
  },
  ttl: 3600,
})

Cache Manager

Manage multiple cache instances:

import { CacheManager, CacheService } from '@hazeljs/cache';

const cacheManager = new CacheManager();

// Register multiple caches
const memoryCache = new CacheService('memory');
const redisCache = new CacheService('redis', { redis: { host: 'localhost' } });

cacheManager.register('memory', memoryCache);
cacheManager.register('redis', redisCache, true); // Set as default

// Use specific cache
const userCache = cacheManager.get('memory');
await userCache.set('key', 'value');

// Use default cache
const defaultCache = cacheManager.get();
await defaultCache.set('key', 'value');

Cache Warming

Pre-populate cache with frequently accessed data:

@Service()
export class CacheWarmupService {
  constructor(private readonly cache: CacheService) {}

  async warmUp() {
    await this.cache.warmUp({
      keys: ['user:1', 'user:2', 'user:3'],
      fetcher: async (key: string) => {
        const userId = key.split(':')[1];
        return await this.userService.findOne(userId);
      },
      ttl: 3600,
      parallel: true, // Fetch in parallel
    });
  }
}

Cache Statistics

Monitor cache performance:

const stats = await cache.getStats();
console.log({
  hits: stats.hits,
  misses: stats.misses,
  hitRate: stats.hitRate,
  size: stats.size,
});

Complete Example

import { Service } from '@hazeljs/core';
import { Controller, Get, Post, Put, Delete, Param, Body } from '@hazeljs/core';
import { 
  CacheService, 
  Cache, 
  CacheEvict, 
  CacheTags,
  // Advanced decorators
  CacheLock,
  CacheAside,
  CacheAsideWithFallback,
  WriteThrough,
  WriteBehind,
  CacheWarm
} from '@hazeljs/cache';

@Service()
export class ProductService {
  constructor(private readonly cache: CacheService) {}

  // Basic cache-aside with automatic get/set
  @CacheAside({
    key: 'product-{id}',
    ttl: 3600
  })
  async findOne(id: string) {
    console.log(`📥 Fetching product ${id} from database`);
    return await this.db.product.findUnique({ where: { id } });
  }

  // Expensive computation with distributed locking
  @CacheLock({
    key: 'product-analysis-{id}',
    ttl: 30000,
    retryDelay: 1000,
    maxRetries: 3
  })
  @Cache({
    key: 'product-analysis-{id}',
    ttl: 7200,
    tags: ['product-analysis']
  })
  async expensiveAnalysis(id: string) {
    console.log(`🔒 Running expensive analysis for product ${id}`);
    await new Promise(resolve => setTimeout(resolve, 3000));
    return {
      productId: id,
      analysis: `Complex analysis for ${id}`,
      timestamp: Date.now()
    };
  }

  // Write-through for immediate cache updates
  @WriteThrough({
    key: 'product-{id}',
    ttl: 3600
  })
  async create(data: CreateProductDto) {
    const product = await this.db.product.create({ data });
    console.log(`✏️ Created product ${product.id} and updated cache`);
    return product;
  }

  // Write-behind for high-performance updates
  @WriteBehind({
    key: 'product-stats-{id}',
    ttl: 1800,
    async: true
  })
  async updateStats(id: string, stats: ProductStatsDto) {
    const updated = await this.db.product.update({
      where: { id },
      data: { stats }
    });
    console.log(`⏱️ Updated stats for product ${id}, cache update queued`);
    return updated;
  }

  // Cache warming for frequently accessed data
  @CacheWarm({
    keys: ['featured-products', 'top-10-products'],
    fetcher: async function(this: ProductService, key: string) {
      console.log(`🔥 Warming cache key: ${key}`);
      if (key === 'featured-products') {
        return await this.db.product.findMany({ where: { featured: true } });
      } else if (key === 'top-10-products') {
        return await this.db.product.findMany({ 
          orderBy: { rating: 'desc' },
          take: 10 
        });
      }
    },
    ttl: 7200,
    parallel: true,
    schedule: '0 */6 * * *' // Every 6 hours
  })
  async warmProductCache() {
    return { message: 'Product cache warming completed' };
  }
}

@Controller('products')
export class ProductsController {
  constructor(private readonly productService: ProductService) {}

  @Get(':id')
  async getProduct(@Param('id') id: string) {
    return await this.productService.findOne(id);
  }

  @Get(':id/analysis')
  async getProductAnalysis(@Param('id') id: string) {
    return await this.productService.expensiveAnalysis(id);
  }

  @Post()
  async createProduct(@Body() createProductDto: CreateProductDto) {
    return await this.productService.create(createProductDto);
  }

  @Put(':id/stats')
  async updateProductStats(
    @Param('id') id: string,
    @Body() statsDto: ProductStatsDto
  ) {
    return await this.productService.updateStats(id, statsDto);
  }

  @CacheEvict({ tags: ['product-analysis'] })
  @Delete(':id')
  async deleteProduct(@Param('id') id: string) {
    return await this.productService.delete(id);
  }
}

// Advanced usage with fallback values
@Service()
export class UserService {
  // Cache-aside with fallback for resilience
  @CacheAsideWithFallback({
    key: 'user-{id}',
    ttl: 1800,
    fallbackValue: { 
      id: 'anonymous', 
      name: 'Guest User', 
      role: 'guest',
      isActive: false
    }
  })
  async getUser(id: string) {
    return await this.db.user.findUnique({ where: { id } });
  }

  // Dynamic fallback based on context
  @CacheAside({
    key: 'user-preferences-{userId}',
    ttl: 3600,
    fallback: async function(this: UserService, userId: string) {
      // Return default preferences based on user type
      const user = await this.getUser(userId);
      return user.role === 'admin' 
        ? this.getAdminDefaultPrefs()
        : this.getUserDefaultPrefs();
    }
  })
  async getUserPreferences(userId: string) {
    return await this.db.preferences.findUnique({ where: { userId } });
  }
}

Integration Examples

Caching RAG Search Results

Dramatically improve RAG performance by caching search results:

import { CacheService } from '@hazeljs/cache';
import { RAGPipeline } from '@hazeljs/rag';

@Service()
export class CachedRAGService {
  constructor(
    private cache: CacheService,
    private rag: RAGPipeline
  ) {}

  async search(query: string, topK: number = 5): Promise<SearchResult[]> {
    const cacheKey = `rag:search:${query}:${topK}`;
    
    // Try cache first
    const cached = await this.cache.get<SearchResult[]>(cacheKey);
    if (cached) {
      return cached;
    }

    // Perform search
    const results = await this.rag.search(query, { topK });

    // Cache with tags for invalidation
    await this.cache.setWithTags(
      cacheKey,
      results,
      3600, // 1 hour
      ['rag-searches', `topK:${topK}`]
    );

    return results;
  }

  async addDocuments(documents: Document[]): Promise<void> {
    await this.rag.addDocuments(documents);
    
    // Invalidate all search caches
    await this.cache.invalidateTags(['rag-searches']);
  }
}

Caching Agent State (Redis)

Use Redis cache for high-performance agent state management:

import { CacheService } from '@hazeljs/cache';
import { AgentContext } from '@hazeljs/agent';

@Service()
export class AgentCacheService {
  private cache: CacheService;

  constructor() {
    this.cache = new CacheService({
      strategy: 'redis',
      redis: {
        host: process.env.REDIS_HOST,
        port: 6379,
      },
    });
  }

  async cacheAgentContext(context: AgentContext): Promise<void> {
    const key = `agent:context:${context.executionId}`;
    const ttl = this.getTTLForState(context.state);
    
    await this.cache.setWithTags(
      key,
      context,
      ttl,
      ['agent-contexts', `session:${context.sessionId}`]
    );
  }

  async getAgentContext(executionId: string): Promise<AgentContext | null> {
    return await this.cache.get<AgentContext>(`agent:context:${executionId}`);
  }

  async invalidateSession(sessionId: string): Promise<void> {
    await this.cache.invalidateTags([`session:${sessionId}`]);
  }

  private getTTLForState(state: string): number {
    switch (state) {
      case 'completed': return 86400; // 24 hours
      case 'failed': return 604800; // 7 days
      default: return 3600; // 1 hour
    }
  }
}

Caching AI Responses

Cache expensive LLM calls:

import { CacheService, Cache } from '@hazeljs/cache';
import { AIService } from '@hazeljs/ai';

@Service()
export class CachedAIService {
  constructor(
    private cache: CacheService,
    private ai: AIService
  ) {}

  @Cache({
    ttl: 7200, // 2 hours
    key: 'ai:response:{prompt}:{model}',
    tags: ['ai-responses'],
  })
  async generateResponse(prompt: string, model: string = 'gpt-4'): Promise<string> {
    const response = await this.ai.executeTask({
      name: 'generate',
      provider: 'openai',
      model,
      prompt,
      outputType: 'string',
    }, {});

    return response.data;
  }

  async clearAICache(): Promise<void> {
    await this.cache.invalidateTags(['ai-responses']);
  }
}

Multi-Tier Caching for RAG

Combine memory and Redis for optimal RAG performance:

import { CacheService } from '@hazeljs/cache';
import { RAGPipeline } from '@hazeljs/rag';

@Service()
export class OptimizedRAGService {
  private cache: CacheService;
  private rag: RAGPipeline;

  constructor() {
    // Multi-tier: L-1 (memory) + L-2 (Redis)
    this.cache = new CacheService({
      strategy: 'multi-tier',
      redis: {
        host: process.env.REDIS_HOST,
        port: 6379,
      },
      ttl: 3600,
    });

    this.rag = new RAGPipeline({
      vectorStore,
      embeddingProvider: embeddings,
    });
  }

  async search(query: string): Promise<SearchResult[]> {
    // L-1 cache (memory) checked first
    // L-2 cache (Redis) checked if L-1 miss
    // RAG search only if both miss
    return await this.cache.getOrSet(
      `rag:search:${query}`,
      async () => await this.rag.search(query),
      3600,
      ['rag-searches']
    );
  }

  async getCacheStats(): Promise<any> {
    const stats = await this.cache.getStats();
    return {
      hitRate: stats.hitRate,
      hits: stats.hits,
      misses: stats.misses,
      size: stats.size,
    };
  }
}

Best Practices

1. Choose the Right Strategy

  • Memory cache: Single-instance apps, development, testing
  • Redis cache: Distributed systems, production, shared state
  • Multi-tier cache: Performance-critical applications with hot data

2. Select Appropriate Caching Patterns

  • Cache-Aside: Read-heavy workloads, eventual consistency OK
  • Write-Through: Critical data, strong consistency required
  • Write-Behind: High-volume writes, performance over consistency
  • Cache-Lock: Expensive operations, prevent stampede
  • Cache-Warming: Predictable access, performance-critical data

3. Set Appropriate TTLs

  • Short TTLs (5-15 min): Frequently changing data, user sessions
  • Medium TTLs (1-4 hours): User profiles, product information
  • Long TTLs (4-24 hours): Configuration, static data, reports
  • Dynamic TTLs: Based on data access patterns and business rules

4. Use Tags for Smart Invalidation

// Good: Group related data
@Cache({ tags: ['users', 'user-{id}'] })
async getUser(id: string) { }

@CacheEvict({ tags: ['user-{id}'] })
async updateUser(id: string, data: any) { }

@CacheEvict({ tags: ['users'] })
async deleteUser(id: string) { }

5. Implement Distributed Locking for Expensive Operations

@CacheLock({
  key: 'expensive-{param}',
  ttl: 60000,
  retryDelay: 2000,
  maxRetries: 5
})
@Cache({ ttl: 3600 })
async expensiveOperation(param: string) {
  // Only one instance runs, others wait for cached result
}

6. Use Write Strategies for Data Consistency

// Critical data - use write-through
@WriteThrough({ key: 'user-{id}', ttl: 1800 })
async updateCriticalUser(id: string, data: any) { }

// High-volume writes - use write-behind
@WriteBehind({ key: 'analytics-{id}', ttl: 300, async: true })
async recordAnalytics(id: string, event: any) { }

7. Implement Cache Warming for Performance

@CacheWarm({
  keys: ['featured-products', 'popular-categories'],
  fetcher: async (key) => await fetchData(key),
  ttl: 7200,
  schedule: '0 2 * * *', // Daily at 2 AM
  condition: 'low-traffic'
})
async warmCache() { }

8. Handle Cache Failures Gracefully

@CacheAsideWithFallback({
  key: 'config-{key}',
  ttl: 3600,
  fallbackValue: getDefaultConfig(key)
})
async getConfig(key: string) {
  return await this.db.config.findUnique({ where: { key } });
}

9. Monitor Cache Performance

  • Track hit rates (aim for >70% in production)
  • Monitor latency and error rates
  • Set up alerts for cache failures
  • Use cache statistics for optimization

10. Namespace Your Keys

// Good: Organized keys
'user:profile:{id}'
'product:details:{slug}'
'analytics:report:{type}:{date}'

// Bad: Unclear keys
'{id}'
'data'
'temp'

11. Avoid Common Pitfalls

  • Don't cache everything: Only cache frequently accessed, expensive data
  • Don't use memory cache in distributed systems: Not shared across instances
  • Don't set TTL too long: Leads to stale data
  • Don't forget invalidation: Update cache when data changes
  • Don't ignore cache failures: Always have fallback logic

12. Test Cache Behavior

// Test cache hit/miss behavior
it('should cache user data', async () => {
  const user = await service.getUser('123');
  const cached = await cacheService.get('user:123');
  expect(cached).toEqual(user);
});

// Test cache invalidation
it('should invalidate cache on update', async () => {
  await service.getUser('123');
  await service.updateUser('123', { name: 'New Name' });
  const cached = await cacheService.get('user:123');
  expect(cached).toBeNull();
});

13. Security Considerations

  • Sensitive data: Don't cache passwords, tokens, or PII
  • Cache encryption: Use Redis SSL for sensitive data in transit
  • Access control: Implement cache-level permissions if needed
  • Cache poisoning: Validate data before caching

14. Production Deployment

// Production cache configuration
CacheModule.forRoot({
  strategy: 'multi-tier',
  redis: {
    host: process.env.REDIS_HOST,
    port: parseInt(process.env.REDIS_PORT),
    password: process.env.REDIS_PASSWORD,
    ssl: process.env.NODE_ENV === 'production'
  },
  ttl: 3600,
  cleanupInterval: 60000,
  monitoring: {
    enabled: true,
    metricsInterval: 30000
  }
});

15. Performance Optimization

  • Use multi-tier cache: L-1 for hot data, L-2 for warm data
  • Implement cache warming: Pre-populate frequently accessed data
  • Optimize key patterns: Use consistent, predictable key formats
  • Batch operations: Use multi-get/set when possible
  • Compression: Enable compression for large cached values

Performance Tips & Benchmarks

Performance Benchmarks

OperationMemory CacheRedis CacheMulti-Tier Cache
GET (cache hit)0.1ms1ms0.1ms (L-1) / 1ms (L-2)
GET (cache miss)N/AN/A1ms (L-2) + DB time
SET0.1ms1ms0.1ms (L-1) + 1ms (L-2)
DELETE0.1ms1ms0.1ms (L-1) + 1ms (L-2)
Lock Acquisition0.05ms2ms0.05ms (L-1) + 2ms (L-2)
Bulk Operations1ms/100 items10ms/100 items1ms/100 (L-1) + 10ms/100 (L-2)

Cache Pattern Performance

Cache-Aside Pattern

  • Hit Rate Impact: 10-100x faster response time
  • Memory Overhead: ~2x data size (metadata + compression)
  • CPU Overhead: Minimal (hash lookup + serialization)
  • Best For: Read-heavy workloads (>70% hit rate)

Write-Through Pattern

  • Write Latency: +1-2ms per write (cache update)
  • Consistency: 100% (cache always matches DB)
  • Memory Usage: Same as cache-aside
  • Best For: Critical data requiring strong consistency

Write-Behind Pattern

  • Write Latency: No additional latency (async)
  • Throughput: 5-10x higher than write-through
  • Consistency: Eventual (seconds to minutes delay)
  • Best For: High-volume writes, analytics, logging

Distributed Locking

  • Lock Acquisition: 0.05ms (memory) / 2ms (Redis)
  • Contention Handling: Configurable retry delays
  • Stampede Prevention: 100% effective
  • Best For: Expensive operations, rate-limited APIs

Cache Warming

  • Warm-up Time: Depends on data size and parallelism
  • Performance Gain: First request becomes cache hit
  • Memory Impact: Temporary spike during warming
  • Best For: Predictable access patterns

Advanced Performance Optimizations

1. Multi-Tier Cache Tuning

// Optimize L-1/L-2 split based on access patterns
CacheModule.forRoot({
  strategy: 'multi-tier',
  lOne: {
    maxSize: 1000,        // Hot data only
    ttl: 300,            // 5 minutes
    evictionPolicy: 'lru' // Evict least recently used
  },
  lTwo: {
    ttl: 3600,           // 1 hour
    maxSize: 100000,      // Warm data
    compression: true    // Enable compression
  }
});

// Expected performance:
// L-1 hits: 0.1ms (80% of requests)
// L-2 hits: 1ms (15% of requests)  
// DB hits: 50ms (5% of requests)
// Average: 0.1ms * 0.8 + 1ms * 0.15 + 50ms * 0.05 = 3.6ms

2. Cache Lock Optimization

@CacheLock({
  key: 'expensive-{param}',
  ttl: 30000,              // 30 seconds - matches operation time
  retryDelay: 500,         // Fast retry for better UX
  maxRetries: 10,          // 5 second total wait
  lockTimeout: 60000       // Auto-expire to prevent deadlocks
})
async expensiveOperation(param: string) {
  // Only one instance runs, others wait efficiently
}

// Performance impact:
// Without lock: N * operation_time (parallel execution)
// With lock: operation_time + (N-1) * cache_hit_time
// For 10 concurrent requests: 10s → 5s (50% improvement)

3. Write-Behind Queue Optimization

@WriteBehind({
  key: 'analytics-{id}',
  ttl: 300,
  async: true,
  batchSize: 100,         // Batch 100 updates together
  batchDelay: 1000,        // Flush every second
  retryAttempts: 3
})
async recordAnalytics(id: string, event: any) {
  // High-throughput analytics recording
}

// Performance gains:
// Individual writes: 1ms each
// Batched writes: 10ms for 100 writes (0.1ms each)
// Throughput improvement: 10x

4. Cache Warming Performance

@CacheWarm({
  keys: async () => await getHotKeys(),
  fetcher: async (key) => await fetchData(key),
  ttl: 7200,
  parallel: 10,           // Warm 10 keys concurrently
  schedule: '0 2 * * *',   // Daily at 2 AM
  condition: 'low-traffic'
})
async warmCache() {
  // Intelligent cache warming
}

// Performance impact:
// Cold start: 100ms per request
// After warming: 1ms per request
// First user experience: 100x improvement

Memory Usage Optimization

1. Compression Strategies

// Enable compression for large values
@Cache({
  key: 'large-data-{id}',
  ttl: 3600,
  compression: {
    enabled: true,
    algorithm: 'gzip',
    threshold: 1024        // Compress values > 1KB
  }
})
async getLargeData(id: string) {
  // 70% reduction in memory usage for large objects
}

2. Selective Caching

// Cache based on data characteristics
@Cache({
  key: 'user-{id}',
  ttl: 1800,
  condition: (user) => user.isActive && user.lastLogin > Date.now() - 30 * 24 * 60 * 60 * 1000
})
async getUser(id: string) {
  // Only cache active, recently logged-in users
  // Reduces memory usage by 60%
}

3. TTL Optimization

// Dynamic TTL based on access patterns
@Cache({
  key: 'product-{id}',
  ttl: async function(this: ProductService, id: string) {
    const stats = await this.getAccessStats(id);
    if (stats.accessCount > 100) return 7200;      // Popular: 2 hours
    if (stats.accessCount > 10) return 3600;       // Moderate: 1 hour
    return 1800;                                   // Normal: 30 minutes
  }
})
async getProduct(id: string) {
  // Optimizes memory usage based on popularity
}

Monitoring Performance Metrics

Key Performance Indicators

interface CachePerformanceMetrics {
  // Latency metrics
  avgGetLatency: number;      // Target: < 1ms
  avgSetLatency: number;      // Target: < 1ms
  p99GetLatency: number;      // Target: < 5ms
  
  // Throughput metrics
  requestsPerSecond: number;  // Monitor capacity
  hitRate: number;            // Target: > 70%
  missRate: number;            // Should decrease over time
  
  // Memory metrics
  memoryUsage: number;        // Monitor growth
  evictionRate: number;       // Should be stable
  fragmentation: number;      // Should be < 20%
  
  // Error metrics
  errorRate: number;          // Target: < 0.1%
  timeoutRate: number;        // Target: < 0.01%
}

Performance Monitoring Implementation

@Service()
export class CachePerformanceService {
  async getPerformanceReport(): Promise<PerformanceReport> {
    const [stats, health, latency] = await Promise.all([
      this.cache.getStats(),
      this.cache.getHealth(),
      this.measureLatency()
    ]);

    return {
      overview: {
        hitRate: stats.hitRate,
        avgLatency: health.latency,
        throughput: this.calculateThroughput(stats),
        errorRate: health.errorRate
      },
      alerts: this.generatePerformanceAlerts(stats, health),
      recommendations: this.generateOptimizationRecommendations(stats)
    };
  }

  private async measureLatency(): Promise<LatencyMetrics> {
    const measurements = [];
    
    for (let i = 0; i < 100; i++) {
      const start = Date.now();
      await this.cache.get(`test-key-${i}`);
      measurements.push(Date.now() - start);
    }

    return {
      avg: measurements.reduce((a, b) => a + b) / measurements.length,
      p50: this.percentile(measurements, 0.5),
      p95: this.percentile(measurements, 0.95),
      p99: this.percentile(measurements, 0.99)
    };
  }

  private generateOptimizationRecommendations(stats: CacheStats): string[] {
    const recommendations = [];

    if (stats.hitRate < 70) {
      recommendations.push('Consider increasing cache TTL or implementing warming');
    }

    if (stats.evictions > 100) {
      recommendations.push('High eviction rate - consider increasing cache size');
    }

    if (stats.memoryUsage > 1000000000) { // 1GB
      recommendations.push('High memory usage - enable compression or review TTL');
    }

    return recommendations;
  }
}

Performance Testing

Load Testing Scenarios

// Cache performance test suite
describe('Cache Performance', () => {
  it('should handle high read load', async () => {
    const promises = Array.from({ length: 10000 }, (_, i) => 
      cache.get(`key-${i % 1000}`) // 1000 unique keys, 10k requests
    );

    const start = Date.now();
    await Promise.all(promises);
    const duration = Date.now() - start;

    expect(duration).toBeLessThan(1000); // < 1 second for 10k requests
    expect(cache.getStats().hitRate).toBeGreaterThan(90);
  });

  it('should handle write-behind throughput', async () => {
    const promises = Array.from({ length: 5000 }, (_, i) => 
      service.recordAnalytics(`user-${i}`, { event: 'action' })
    );

    const start = Date.now();
    await Promise.all(promises);
    const duration = Date.now() - start;

    expect(duration).toBeLessThan(2000); // < 2 seconds for 5k writes
  });

  it('should prevent cache stampede', async () => {
    const promises = Array.from({ length: 10 }, () => 
      service.expensiveOperation('test')
    );

    const results = await Promise.all(promises);
    
    // Only one should compute, others get cached result
    const computations = results.filter(r => r.computed === true);
    expect(computations).toHaveLength(1);
  });
});

Production Performance Checklist

  • Hit Rate: >70% for production workloads
  • Latency: &lt;1ms average, &lt;5ms p99
  • Memory Usage: &lt;80% of allocated memory
  • Error Rate: &lt;0.1% for cache operations
  • Eviction Rate: Stable and predictable
  • Lock Contention: &lt;5% of requests waiting for locks
  • Write-Behind Queue: &lt;1000 pending items
  • Cache Warming: Completes within maintenance window
  • Monitoring: All KPIs tracked and alerted
  • Backup: Redis persistence configured

Performance Troubleshooting

Common Performance Issues

  • Low Hit Rate (<70%)

    • Check TTL settings
    • Review cache key patterns
    • Implement cache warming
    • Analyze access patterns
  • High Latency (>5ms p99)

    • Check Redis connection pool
    • Monitor network latency
    • Review serialization overhead
    • Consider memory-only cache for hot data
  • High Memory Usage

    • Enable compression
    • Review TTL settings
    • Implement eviction policies
    • Monitor cache key growth
  • Lock Contention

    • Increase lock timeout
    • Optimize expensive operations
    • Implement request coalescing
    • Review lock key patterns
  • Write-Behind Queue Buildup

    • Increase batch size
    • Add more workers
    • Monitor Redis performance
    • Review write patterns

What's Next?

Recipes

Recipe: Cache Database Queries

// File: src/products/products.service.ts
import { Service } from '@hazeljs/core';
import { CacheService } from '@hazeljs/cache';
import { PrismaService } from '@hazeljs/prisma';

@Service()
export class ProductsService {
  constructor(
    private readonly cache: CacheService,
    private readonly prisma: PrismaService,
  ) {}

  async findAll() {
    const cached = await this.cache.get('products:all');
    if (cached) return cached;

    const products = await this.prisma.product.findMany();
    await this.cache.set('products:all', products, { ttl: 300 });
    return products;
  }

  async create(data: { name: string; price: number }) {
    const product = await this.prisma.product.create({ data });
    await this.cache.del('products:all');
    return product;
  }
}

Recipe: Cache AI Responses to Reduce Cost

// File: src/ai/cached-ai.service.ts
import { Service } from '@hazeljs/core';
import { CacheService } from '@hazeljs/cache';
import { AIEnhancedService } from '@hazeljs/ai';
import { createHash } from 'crypto';

@Service()
export class CachedAIService {
  constructor(
    private readonly cache: CacheService,
    private readonly ai: AIEnhancedService,
  ) {}

  async generate(prompt: string): Promise<string> {
    const algorithm = 'sha256';
    const key = `ai:${createHash(algorithm).update(prompt).digest('hex')}`;
    const cached = await this.cache.get<string>(key);
    if (cached) return cached;

    const response = await this.ai.chat(prompt).text();
    await this.cache.set(key, response, { ttl: 3600 });
    return response;
  }
}