HazelJS Cache Package
@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/cacheprovides 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/cacheto 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, optionallyioredisfor 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 namedparamName{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
@Cacheif 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: trueto 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(ifbeforeInvocation: true)- Cache lookup (if
@Cacheis present) - Method execution (if cache miss)
- Result caching (if
@Cacheis present) @CacheEvict(ifbeforeInvocation: 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 automaticallyWrite-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:#e8f5e8Distributed 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 resultMulti-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:#fef3c7Cache 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:#fffCache 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
| Operation | Memory Cache | Redis Cache | Multi-Tier Cache |
|---|---|---|---|
| GET (cache hit) | 0.1ms | 1ms | 0.1ms (L-1) / 1ms (L-2) |
| GET (cache miss) | N/A | N/A | 1ms (L-2) + DB time |
| SET | 0.1ms | 1ms | 0.1ms (L-1) + 1ms (L-2) |
| DELETE | 0.1ms | 1ms | 0.1ms (L-1) + 1ms (L-2) |
| Lock Acquisition | 0.05ms | 2ms | 0.05ms (L-1) + 2ms (L-2) |
| Bulk Operations | 1ms/100 items | 10ms/100 items | 1ms/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:
<1msaverage,<5msp99 - Memory Usage:
<80%of allocated memory - Error Rate:
<0.1%for cache operations - Eviction Rate: Stable and predictable
- Lock Contention:
<5%of requests waiting for locks - Write-Behind Queue:
<1000pending 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?
- Learn about Agent Package for Redis state management
- Explore RAG Package for caching search results
- Check out AI Package for caching LLM responses
- Read Caching Strategies for advanced patterns
- Read Config for cache configuration
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;
}
}
Related Resources
- AI Package – LLM response caching
- RAG Package – Search result caching
- Agent Package – Agent state caching
- Queue Package – Redis-based job queues
- Config Package – Cache configuration
- hazeljs-csr-example – Client-side rendering with caching