Advanced Caching Strategies Guide
This guide covers advanced caching patterns and strategies for building high-performance applications with HazelJS. You'll learn when and how to use different caching approaches, from basic method caching to sophisticated distributed locking and cache warming strategies.
Table of Contents
- Understanding Caching Patterns
- Basic Caching Setup
- Cache-Aside Pattern
- Write Strategies
- Distributed Locking
- Cache Warming
- Multi-Tier Caching
- Performance Optimization
- Monitoring and Debugging
- Common Pitfalls
Understanding Caching Patterns
Caching is not one-size-fits-all. Different scenarios require different caching strategies. Let's explore the most common patterns and when to use them.
Cache Patterns Overview
| Pattern | Use Case | Consistency | Performance | Complexity |
|---|---|---|---|---|
| Cache-Aside | Read-heavy workloads | Eventual | High | Low |
| Write-Through | Critical data | Strong | Medium | Low |
| Write-Behind | High-volume writes | Eventual | Very High | Medium |
| Cache-Lock | Expensive computations | Strong | High | Medium |
| Cache-Warming | Predictable access | Eventual | Very High | High |
Choosing the Right Pattern
// Decision matrix for caching patterns
const chooseCachingPattern = (scenario: string) => {
switch (scenario) {
case 'user-profile-reads':
return 'Cache-Aside'; // High read-to-write ratio
case 'financial-transactions':
return 'Write-Through'; // Strong consistency required
case 'analytics-events':
return 'Write-Behind'; // High write volume, eventual consistency OK
case 'report-generation':
return 'Cache-Lock + Cache-Warming'; // Expensive, predictable access
default:
return 'Cache-Aside'; // Default safe choice
}
};
Basic Caching Setup
Before diving into advanced patterns, let's set up a solid foundation.
Step 1: Install and Configure
npm install @hazeljs/cache
# For Redis support
npm install ioredis
Step 2: Module Configuration
// src/app.module.ts
import { HazelModule } from '@hazeljs/core';
import { CacheModule } from '@hazeljs/cache';
@HazelModule({
imports: [
CacheModule.forRoot({
strategy: 'multi-tier', // Best for production
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
},
ttl: 3600, // Default 1 hour
cleanupInterval: 60000, // Cleanup every minute
}),
],
})
export class AppModule {}
Step 3: Basic Service Setup
// src/services/base.service.ts
import { Service } from '@hazeljs/core';
import { CacheService } from '@hazeljs/cache';
@Service()
export class BaseService {
constructor(protected readonly cache: CacheService) {}
protected async getOrSet<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 3600,
tags: string[] = []
): Promise<T> {
return await this.cache.getOrSet(key, fetcher, ttl, tags);
}
protected async invalidate(tags: string[]): Promise<void> {
await this.cache.invalidateTags(tags);
}
}
Cache-Aside Pattern
The cache-aside pattern is the most common and versatile caching strategy. It's perfect for read-heavy workloads where eventual consistency is acceptable.
When to Use Cache-Aside
- Read-heavy applications: More reads than writes (3:1 ratio or higher)
- User data: Profiles, preferences, settings
- Product catalogs: E-commerce product information
- Configuration data: Application settings, feature flags
Implementation Examples
Basic Cache-Aside
// src/services/user.service.ts
import { Service } from '@hazeljs/core';
import { CacheAside } from '@hazeljs/cache';
@Service()
export class UserService extends BaseService {
@CacheAside({
key: 'user-{id}',
ttl: 1800, // 30 minutes
})
async getUser(id: string) {
console.log(`📥 Fetching user ${id} from database`);
return await this.db.user.findUnique({ where: { id } });
}
@CacheAside({
key: 'user-profile-{id}',
ttl: 3600, // 1 hour
})
async getUserProfile(id: string) {
const user = await this.getUser(id);
const profile = await this.db.profile.findUnique({
where: { userId: id }
});
return { user, profile };
}
}
Cache-Aside with Fallback
// src/services/product.service.ts
import { CacheAsideWithFallback } from '@hazeljs/cache';
@Service()
export class ProductService extends BaseService {
@CacheAsideWithFallback({
key: 'product-{id}',
ttl: 3600,
fallbackValue: {
id: 'unknown',
name: 'Product Not Found',
price: 0,
available: false
}
})
async getProduct(id: string) {
return await this.db.product.findUnique({ where: { id } });
}
// Dynamic fallback based on context
@CacheAside({
key: 'product-recommendations-{userId}',
ttl: 7200, // 2 hours
fallback: async function(this: ProductService, userId: string) {
// Get user's category preferences
const user = await this.getUser(userId);
const category = user.preferredCategory || 'electronics';
// Return generic recommendations for that category
return await this.getGenericRecommendations(category);
}
})
async getRecommendations(userId: string) {
return await this.recommendationEngine.getPersonalized(userId);
}
}
Cache-Aside Best Practices
- Use descriptive keys:
user-{id},product-{slug},config-{key} - Set appropriate TTLs: Shorter for frequently changing data
- Use tags for invalidation: Group related cache entries
- Handle cache failures gracefully: Always have fallback logic
Write Strategies
Write strategies determine how you handle cache updates when data changes. The choice between write-through and write-behind depends on your consistency requirements and performance needs.
Write-Through Caching
Write-through caching updates the cache immediately when data changes. This provides strong consistency but can impact write performance.
When to Use Write-Through
- Critical data: User authentication, financial transactions
- Real-time requirements: Chat messages, live updates
- Consistency over performance: When stale data is unacceptable
Implementation
// src/services/account.service.ts
import { WriteThrough } from '@hazeljs/cache';
@Service()
export class AccountService extends BaseService {
@WriteThrough({
key: 'account-{id}',
ttl: 3600,
})
async updateAccount(id: string, data: UpdateAccountDto) {
console.log(`✏️ Updating account ${id} in database and cache`);
// 1. Update database
const updated = await this.db.account.update({
where: { id },
data: {
...data,
updatedAt: new Date()
}
});
// 2. Cache is automatically updated by decorator
return updated;
}
@WriteThrough({
key: 'user-balance-{userId}',
ttl: 300, // 5 minutes - financial data should be fresh
})
async updateBalance(userId: string, amount: number) {
// Financial update - must be immediately consistent
const updated = await this.db.account.update({
where: { userId },
data: { balance: { increment: amount } }
});
// Cache updated immediately with new balance
return updated;
}
}
Write-Behind Caching
Write-behind caching queues cache updates for asynchronous processing. This provides excellent write performance with eventual consistency.
When to Use Write-Behind
- High-volume writes: Analytics events, logging
- Performance-critical writes: Real-time gaming, IoT data
- Eventual consistency is acceptable: Non-critical data
Implementation
// src/services/analytics.service.ts
import { WriteBehind } from '@hazeljs/cache';
@Service()
export class AnalyticsService extends BaseService {
@WriteBehind({
key: 'page-views-{url}',
ttl: 300, // 5 minutes
async: true,
})
async recordPageView(url: string, userId?: string) {
console.log(`📊 Recording page view for ${url}`);
// Update database immediately
const updated = await this.db.pageView.upsert({
where: { url },
update: {
count: { increment: 1 },
lastViewed: new Date()
},
create: {
url,
count: 1,
lastViewed: new Date()
}
});
// Cache update happens asynchronously
return updated;
}
@WriteBehind({
key: 'user-events-{userId}',
ttl: 1800, // 30 minutes
async: true,
})
async trackUserEvent(userId: string, event: UserEvent) {
// High-frequency event tracking
const updated = await this.db.userEvent.create({
data: {
userId,
type: event.type,
data: event.data,
timestamp: new Date()
}
});
// Cache updated asynchronously for better performance
return updated;
}
// Batch write-behind for bulk operations
@WriteBehind({
key: 'batch-metrics-{batchId}',
ttl: 3600,
async: true,
})
async processBatchMetrics(batchId: string, metrics: Metric[]) {
// Process large batch of metrics
const results = await Promise.all(
metrics.map(metric =>
this.db.metric.create({
data: { ...metric, batchId }
})
)
);
return { processed: results.length, batchId };
}
}
Write Strategy Comparison
| Aspect | Write-Through | Write-Behind |
|---|---|---|
| Consistency | Strong | Eventual |
| Write Performance | Good | Excellent |
| Cache Freshness | Immediate | Delayed |
| Complexity | Low | Medium |
| Use Case | Critical data | High-volume writes |
Distributed Locking
Distributed locking prevents cache stampede and ensures expensive operations run only once across multiple instances.
When to Use Distributed Locking
- Expensive computations: Report generation, data analysis
- Rate-limited APIs: External service calls with limits
- Resource-intensive operations: File processing, image generation
- Preventing thundering herd: Multiple requests for same data
Implementation Examples
Basic Cache Lock
// src/services/report.service.ts
import { CacheLock } from '@hazeljs/cache';
@Service()
export class ReportService extends BaseService {
@CacheLock({
key: 'report-{type}-{date}',
ttl: 60000, // 1 minute lock
retryDelay: 2000, // Check every 2 seconds
maxRetries: 10, // Try up to 10 times (20 seconds total)
})
@Cache({
key: 'report-{type}-{date}',
ttl: 7200, // Cache result for 2 hours
tags: ['reports', 'reports-{type}']
})
async generateReport(type: string, date: string) {
console.log(`🔒 Generating ${type} report for ${date}`);
// Expensive report generation
await new Promise(resolve => setTimeout(resolve, 10000));
const report = {
type,
date,
data: `Report data for ${type} on ${date}`,
generatedAt: new Date().toISOString(),
size: Math.floor(Math.random() * 10000) // Simulate report size
};
return report;
}
@CacheLock({
key: 'export-{format}-{userId}',
ttl: 300000, // 5 minutes for large exports
retryDelay: 5000,
maxRetries: 6, // 30 seconds total wait time
})
async exportUserData(format: 'csv' | 'json' | 'pdf', userId: string) {
console.log(`📤 Exporting user ${userId} data as ${format}`);
// Simulate large data export
const userData = await this.db.user.findUnique({ where: { id: userId } });
const userActions = await this.db.userAction.findMany({
where: { userId },
take: 10000
});
// Process export based on format
let exportData;
switch (format) {
case 'csv':
exportData = this.generateCSV(userData, userActions);
break;
case 'json':
exportData = this.generateJSON(userData, userActions);
break;
case 'pdf':
exportData = await this.generatePDF(userData, userActions);
break;
}
return {
userId,
format,
size: exportData.length,
downloadUrl: `/downloads/${userId}-export.${format}`,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
};
}
}
Advanced Lock Patterns
// src/services/ai.service.ts
@Service()
export class AIService extends BaseService {
@CacheLock({
key: 'ai-analysis-{type}-{hash}',
ttl: 120000, // 2 minutes for AI processing
retryDelay: 3000,
maxRetries: 20, // 1 minute total wait
})
@Cache({
key: 'ai-analysis-{type}-{hash}',
ttl: 14400, // 4 hours cache for AI results
tags: ['ai-analysis', 'ai-{type}']
})
async analyzeContent(type: 'sentiment' | 'classification' | 'extraction', content: string) {
console.log(`🤖 Running AI ${type} analysis`);
// Generate hash for content to avoid duplicate analysis
const hash = this.createHash(content);
// Call external AI service (rate limited and expensive)
const result = await this.aiProvider.analyze({
type,
content,
model: 'gpt-4'
});
return {
type,
contentHash: hash,
result,
confidence: result.confidence,
processedAt: new Date().toISOString()
};
}
@CacheLock({
key: 'batch-processing-{batchId}',
ttl: 600000, // 10 minutes for batch processing
retryDelay: 10000,
maxRetries: 6, // 1 minute total
})
async processBatch(batchId: string) {
console.log(`🔄 Processing batch ${batchId}`);
const batch = await this.db.batch.findUnique({ where: { id: batchId } });
if (!batch) throw new Error('Batch not found');
// Process all items in batch
const results = await Promise.all(
batch.items.map(async (item, index) => {
console.log(`Processing item ${index + 1}/${batch.items.length}`);
return await this.processItem(item);
})
);
// Update batch status
await this.db.batch.update({
where: { id: batchId },
data: {
status: 'completed',
completedAt: new Date(),
resultCount: results.length
}
});
return { batchId, processed: results.length };
}
private createHash(content: string): string {
const crypto = require('crypto');
const algorithm = 'sha256';
return crypto.createHash(algorithm).update(content).digest('hex').substring(0, 16);
}
}
Lock Management Best Practices
- Set appropriate lock TTLs: Long enough for operation, short enough to prevent deadlocks
- Configure retry strategies: Balance between responsiveness and resource usage
- Monitor lock contention: High contention may indicate performance issues
- Handle lock failures gracefully: Provide fallback or retry logic
Cache Warming
Cache warming pre-populates cache with frequently accessed data, improving performance for predictable access patterns.
When to Use Cache Warming
- Predictable access patterns: Daily reports, popular products
- Expensive computations: Complex analytics, AI processing
- Performance-critical applications: E-commerce, real-time dashboards
- Scheduled operations: Batch jobs, periodic reports
Implementation Examples
Scheduled Cache Warming
// src/services/dashboard.service.ts
import { CacheWarm, CacheWarmingUtils } from '@hazeljs/cache';
@Service()
export class DashboardService extends BaseService {
@CacheWarm({
keys: [
'dashboard-overview',
'sales-summary',
'user-metrics',
'product-performance'
],
fetcher: async function(this: DashboardService, key: string) {
console.log(`🔥 Warming dashboard cache: ${key}`);
switch (key) {
case 'dashboard-overview':
return await this.getOverviewData();
case 'sales-summary':
return await this.getSalesSummary();
case 'user-metrics':
return await this.getUserMetrics();
case 'product-performance':
return await this.getProductPerformance();
default:
return null;
}
},
ttl: 3600, // 1 hour
parallel: true, // Warm all keys in parallel
schedule: '0 8 * * *', // Every day at 8 AM
condition: 'business-hours' // Only during business hours
})
async warmDashboardCache() {
console.log('🌡️ Dashboard cache warming completed');
return { message: 'Dashboard cache warmed successfully' };
}
private async getOverviewData() {
// Expensive overview query
const [users, products, orders, revenue] = await Promise.all([
this.db.user.count(),
this.db.product.count(),
this.db.order.count({ where: { status: 'completed' } }),
this.db.order.aggregate({ _sum: { total: true } })
]);
return { users, products, orders, revenue: revenue._sum.total };
}
private async getSalesSummary() {
const today = new Date();
const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
return await this.db.order.groupBy({
by: ['status'],
where: {
createdAt: { gte: lastMonth }
},
_sum: { total: true },
_count: true
});
}
private async getUserMetrics() {
return await this.db.user.aggregate({
_count: true,
_avg: { rating: true },
where: { isActive: true }
});
}
private async getProductPerformance() {
return await this.db.product.findMany({
orderBy: { salesCount: 'desc' },
take: 10,
select: {
id: true,
name: true,
salesCount: true,
rating: true,
category: true
}
});
}
}
Dynamic Cache Warming
// src/services/content.service.ts
@Service()
export class ContentService extends BaseService {
@CacheWarm({
keys: [
'trending-articles',
'featured-content',
'popular-categories'
],
fetcher: async function(this: ContentService, key: string) {
console.log(`🔥 Warming content cache: ${key}`);
switch (key) {
case 'trending-articles':
return await this.getTrendingArticles();
case 'featured-content':
return await this.getFeaturedContent();
case 'popular-categories':
return await this.getPopularCategories();
default:
return null;
}
},
ttl: 7200, // 2 hours
parallel: true,
schedule: '*/30 * * * *', // Every 30 minutes
condition: 'high-traffic' // Only during high traffic periods
})
async warmContentCache() {
return { message: 'Content cache warmed' };
}
// Context-aware warming based on user behavior
@CacheWarm({
keys: async function(this: ContentService) {
// Dynamically determine keys based on recent activity
const recentViews = await this.getRecentViews();
const trendingTopics = await this.getTrendingTopics();
return [
...recentViews.map(view => `article-${view.articleId}`),
...trendingTopics.map(topic => `topic-${topic.id}-articles`)
];
},
fetcher: async function(this: ContentService, key: string) {
if (key.startsWith('article-')) {
const articleId = key.replace('article-', '');
return await this.getArticle(articleId);
} else if (key.startsWith('topic-')) {
const parts = key.split('-');
const topicId = parts[1];
return await this.getTopicArticles(topicId);
}
},
ttl: 3600,
parallel: true,
schedule: '0,15,30,45 * * * *' // Every 15 minutes
})
async warmDynamicContent() {
return { message: 'Dynamic content warmed' };
}
private async getRecentViews() {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
return await this.db.view.findMany({
where: { createdAt: { gte: oneHourAgo } },
select: { articleId: true },
distinct: ['articleId'],
take: 50
});
}
private async getTrendingTopics() {
return await this.db.topic.findMany({
orderBy: { trendingScore: 'desc' },
take: 10
});
}
}
Manual Cache Warming
// src/controllers/admin.controller.ts
import { Controller, Post, Get, Body } from '@hazeljs/core';
import { CacheWarmingUtils } from '@hazeljs/cache';
@Controller('admin/cache')
export class CacheController {
@Post('warm')
async warmCache(@Body() body: { job?: string }) {
try {
const jobs = CacheWarmingUtils.listJobs();
if (body.job) {
// Warm specific job
const job = jobs.find(j => j.name === body.job);
if (!job) {
return { success: false, message: 'Job not found' };
}
await CacheWarmingUtils.warmUp(job);
return { success: true, message: `Warmed job: ${body.job}` };
} else {
// Warm all jobs
await Promise.all(jobs.map(job => CacheWarmingUtils.warmUp(job)));
return { success: true, message: `Warmed ${jobs.length} jobs` };
}
} catch (error) {
return { success: false, error: error.message };
}
}
@Get('jobs')
listWarmingJobs() {
return {
jobs: CacheWarmingUtils.listJobs(),
stats: CacheWarmingUtils.getStats()
};
}
@Post('clear')
clearWarmingJobs() {
CacheWarmingUtils.destroy();
return { success: true, message: 'Warming jobs cleared' };
}
}
Cache Warming Strategies
- Scheduled Warming: Use cron patterns for predictable warming
- Conditional Warming: Warm based on traffic, time, or business conditions
- Dynamic Warming: Generate keys based on current usage patterns
- Manual Warming: Provide admin controls for on-demand warming
Multi-Tier Caching
Multi-tier caching combines multiple cache layers for optimal performance and cost-effectiveness.
Architecture Overview
Application
↓
L-1 Cache (Memory) - Fastest, smallest
↓ (miss)
L-2 Cache (Redis) - Fast, medium size
↓ (miss)
Database - Slowest, largest
When to Use Multi-Tier
- High-performance requirements: Sub-millisecond response needed
- Cost optimization: Reduce database load while controlling memory usage
- Distributed systems: Shared cache across multiple instances
- Hot data patterns: Small subset of data accessed frequently
Implementation
// src/app.module.ts
import { HazelModule } from '@hazeljs/core';
import { CacheModule } from '@hazeljs/cache';
@HazelModule({
imports: [
CacheModule.forRoot({
strategy: 'multi-tier',
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT),
},
memory: {
maxSize: 1000, // Maximum entries in L-1 cache
ttl: 300, // 5 minutes for L-1
},
redis: {
ttl: 3600, // 1 hour for L-2
},
// L-1 gets 5 minutes, L-2 gets 1 hour
lOneTTL: 300,
lTwoTTL: 3600,
}),
],
})
export class AppModule {}
Multi-Tier Service Implementation
// src/services/product.service.ts
@Service()
export class ProductService extends BaseService {
// Hot products - cached in L-1 for instant access
@Cache({
key: 'hot-product-{id}',
ttl: 300, // 5 minutes (L-1)
tags: ['hot-products']
})
async getHotProduct(id: string) {
return await this.db.product.findUnique({
where: { id, isHot: true }
});
}
// Regular products - cached in L-2
@Cache({
key: 'product-{id}',
ttl: 3600, // 1 hour (L-2)
tags: ['products']
})
async getProduct(id: string) {
return await this.db.product.findUnique({ where: { id } });
}
// Search results - medium cache duration
@Cache({
key: 'search-{query}-{page}',
ttl: 1800, // 30 minutes
tags: ['search-results']
})
async searchProducts(query: string, page: number = 1) {
return await this.db.product.findMany({
where: {
OR: [
{ name: { contains: query } },
{ description: { contains: query } },
{ category: { contains: query } }
]
},
skip: (page - 1) * 20,
take: 20
});
}
// Analytics - longer cache, L-2 only
@Cache({
key: 'product-analytics-{id}',
ttl: 7200, // 2 hours
tags: ['analytics']
})
async getProductAnalytics(id: string) {
const [views, purchases, ratings] = await Promise.all([
this.db.view.count({ where: { productId: id } }),
this.db.purchase.count({ where: { productId: id } }),
this.db.rating.aggregate({
where: { productId: id },
_avg: { score: true }
})
]);
return { views, purchases, avgRating: ratings._avg.score };
}
}
Tier-Specific Operations
// src/services/cache-admin.service.ts
@Service()
export class CacheAdminService extends BaseService {
// Clear specific tier
async clearTier(tier: 'l-1' | 'l-2' | 'all') {
switch (tier) {
case 'l-1':
await this.cache.clearLOne();
break;
case 'l-2':
await this.cache.clearLTwo();
break;
case 'all':
await this.cache.clear();
break;
}
return { cleared: tier };
}
// Get tier-specific statistics
async getTierStats() {
const lOneStats = await this.cache.getLOneStats();
const lTwoStats = await this.cache.getLTwoStats();
return {
'l-1': {
size: lOneStats.size,
hitRate: lOneStats.hitRate,
memoryUsage: lOneStats.memoryUsage
},
'l-2': {
size: lTwoStats.size,
hitRate: lTwoStats.hitRate,
memoryUsage: lTwoStats.memoryUsage
},
combined: {
totalHitRate: this.calculateCombinedHitRate(lOneStats, lTwoStats)
}
};
}
// Promote data from L-2 to L-1
async promoteToLOne(key: string) {
const value = await this.cache.getFromLTwo(key);
if (value) {
await this.cache.setToLOne(key, value, 300); // 5 minutes
return { promoted: key };
}
return { promoted: null, reason: 'not_found_in_l_two' };
}
private calculateCombinedHitRate(lOne: any, lTwo: any): number {
const totalRequests = lOne.hits + lOne.misses + lTwo.hits + lTwo.misses;
const totalHits = lOne.hits + lTwo.hits;
return totalRequests > 0 ? (totalHits / totalRequests) * 100 : 0;
}
}
Performance Optimization
Optimizing cache performance involves monitoring, tuning, and implementing best practices.
Key Performance Metrics
// src/services/cache-metrics.service.ts
@Service()
export class CacheMetricsService extends BaseService {
async getPerformanceReport() {
const stats = await this.cache.getStats();
const health = await this.cache.getHealth();
return {
performance: {
hitRate: stats.hitRate,
avgLatency: health.latency,
errorRate: health.errorRate,
throughput: this.calculateThroughput(stats)
},
memory: {
usage: health.memoryUsage,
efficiency: this.calculateMemoryEfficiency(stats),
fragmentation: this.calculateFragmentation(health)
},
recommendations: this.generateRecommendations(stats, health)
};
}
private calculateThroughput(stats: CacheStats): number {
// Requests per second
const timeWindow = 60; // 1 minute
return (stats.hits + stats.misses) / timeWindow;
}
private calculateMemoryEfficiency(stats: CacheStats): number {
// Bytes per entry
return stats.memoryUsage ? stats.memoryUsage / stats.size : 0;
}
private calculateFragmentation(health: CacheHealth): number {
// Estimate fragmentation based on memory patterns
return health.memoryUsage * 0.1; // Simplified calculation
}
private generateRecommendations(stats: CacheStats, health: CacheHealth): string[] {
const recommendations: string[] = [];
if (stats.hitRate < 70) {
recommendations.push('Consider increasing cache TTL or warming strategies');
}
if (health.latency > 10) {
recommendations.push('Cache latency is high, consider memory-only cache for hot data');
}
if (health.errorRate > 0.01) {
recommendations.push('High error rate detected, check cache connectivity');
}
if (stats.size > 10000) {
recommendations.push('Large cache size, consider implementing eviction policies');
}
return recommendations;
}
}
Performance Tuning Strategies
1. TTL Optimization
// Dynamic TTL based on access patterns
@Service()
export class SmartCacheService extends BaseService {
@Cache({
key: 'user-{id}',
ttl: async function(this: SmartCacheService, id: string) {
// Dynamic TTL based on user activity
const user = await this.getUserActivity(id);
if (user.isActive) {
return 1800; // 30 minutes for active users
} else if (user.recentlyActive) {
return 7200; // 2 hours for recently active
} else {
return 86400; // 24 hours for inactive users
}
}
})
async getUser(id: string) {
return await this.db.user.findUnique({ where: { id } });
}
private async getUserActivity(id: string) {
const lastLogin = await this.db.user.findUnique({
where: { id },
select: { lastLoginAt: true }
});
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
return {
isActive: lastLogin?.lastLoginAt > oneDayAgo,
recentlyActive: lastLogin?.lastLoginAt > oneWeekAgo
};
}
}
2. Cache Partitioning
// Partition cache by data type and access pattern
@Service()
export class PartitionedCacheService extends BaseService {
// Hot data - small TTL, high priority
@Cache({
key: 'hot-{type}-{id}',
ttl: 300,
tags: ['hot-data']
})
async getHotData(type: string, id: string) {
return await this.getData(type, id);
}
// Warm data - medium TTL
@Cache({
key: 'warm-{type}-{id}',
ttl: 1800,
tags: ['warm-data']
})
async getWarmData(type: string, id: string) {
return await this.getData(type, id);
}
// Cold data - long TTL, low priority
@Cache({
key: 'cold-{type}-{id}',
ttl: 86400,
tags: ['cold-data']
})
async getColdData(type: string, id: string) {
return await this.getData(type, id);
}
private async getData(type: string, id: string) {
// Determine data temperature and route to appropriate cache
const accessStats = await this.getAccessStats(type, id);
if (accessStats.accessCount > 100) {
return await this.getHotData(type, id);
} else if (accessStats.accessCount > 10) {
return await this.getWarmData(type, id);
} else {
return await this.getColdData(type, id);
}
}
}
Monitoring and Debugging
Effective monitoring helps you understand cache performance and identify issues quickly.
Cache Monitoring Dashboard
// src/services/cache-monitor.service.ts
@Service()
export class CacheMonitorService extends BaseService {
async getDashboardData() {
const [stats, health, recentEvents] = await Promise.all([
this.cache.getStats(),
this.cache.getHealth(),
this.getRecentEvents()
]);
return {
overview: {
totalEntries: stats.size,
hitRate: stats.hitRate,
errorRate: health.errorRate,
latency: health.latency,
memoryUsage: health.memoryUsage
},
performance: {
hits: stats.hits,
misses: stats.misses,
evictions: stats.evictions || 0,
throughput: this.calculateThroughput(stats)
},
health: {
status: health.status,
uptime: health.uptime,
lastCheck: health.lastCheck
},
events: recentEvents.slice(0, 50), // Last 50 events
alerts: this.generateAlerts(stats, health)
};
}
private async getRecentEvents() {
// Get recent cache events for debugging
return await this.cache.getEvents({
limit: 100,
types: ['hit', 'miss', 'eviction', 'error']
});
}
private generateAlerts(stats: CacheStats, health: CacheHealth) {
const alerts = [];
if (stats.hitRate < 50) {
alerts.push({
level: 'warning',
message: 'Low cache hit rate detected',
value: `${stats.hitRate}%`
});
}
if (health.errorRate > 0.05) {
alerts.push({
level: 'error',
message: 'High cache error rate',
value: `${(health.errorRate * 100).toFixed(2)}%`
});
}
if (health.latency > 100) {
alerts.push({
level: 'warning',
message: 'High cache latency',
value: `${health.latency}ms`
});
}
return alerts;
}
}
Debugging Tools
// src/services/cache-debug.service.ts
@Service()
export class CacheDebugService extends BaseService {
async traceKey(key: string) {
const entry = await this.cache.getEntry(key);
const metadata = await this.cache.getMetadata(key);
return {
key,
exists: !!entry,
entry: entry ? {
value: entry.value,
cachedAt: entry.cachedAt,
expiresAt: entry.expiresAt,
lastAccessedAt: entry.lastAccessedAt,
tags: entry.tags
} : null,
metadata,
recommendations: this.analyzeEntry(entry, metadata)
};
}
async analyzePattern(pattern: string) {
const keys = await this.cache.keys(pattern);
const analysis = await Promise.all(
keys.map(async key => this.traceKey(key))
);
return {
pattern,
totalKeys: keys.length,
totalSize: analysis.reduce((sum, item) =>
sum + (item.entry ? JSON.stringify(item.entry.value).length : 0), 0
),
avgTTL: this.calculateAverageTTL(analysis),
hotKeys: this.findHotKeys(analysis),
staleKeys: this.findStaleKeys(analysis),
recommendations: this.generatePatternRecommendations(analysis)
};
}
private analyzeEntry(entry: any, metadata: any) {
const recommendations = [];
if (!entry) {
recommendations.push('Key not found in cache');
return recommendations;
}
const now = Date.now();
const age = now - entry.cachedAt;
const ttl = entry.expiresAt - entry.cachedAt;
if (age > ttl * 0.8) {
recommendations.push('Entry is near expiration, consider refreshing');
}
if (!entry.lastAccessedAt || (now - entry.lastAccessedAt) > ttl * 0.5) {
recommendations.push('Entry hasn't been accessed recently, consider longer TTL');
}
if (metadata?.accessCount < 5) {
recommendations.push('Low access count, consider if caching is necessary');
}
return recommendations;
}
private findHotKeys(analysis: any[]) {
return analysis
.filter(item => item.metadata?.accessCount > 50)
.map(item => ({
key: item.key,
accessCount: item.metadata.accessCount,
hitRate: item.metadata.hitRate
}));
}
private findStaleKeys(analysis: any[]) {
const now = Date.now();
return analysis
.filter(item => {
if (!item.entry) return false;
const age = now - item.entry.cachedAt;
const ttl = item.entry.expiresAt - item.entry.cachedAt;
return age > ttl * 0.9;
})
.map(item => item.key);
}
}
Common Pitfalls and Solutions
1. Cache Stampede
Problem: Multiple requests simultaneously computing the same expensive value.
Solution: Use distributed locking.
@CacheLock({
key: 'expensive-{param}',
ttl: 30000
})
async expensiveOperation(param: string) {
// Only one instance runs at a time
return await computeExpensiveValue(param);
}
2. Stale Data
Problem: Cache contains outdated information.
Solution: Implement proper invalidation strategies.
@Service()
export class ProductService {
@CacheEvict({ tags: ['products'] })
async updateProduct(id: string, data: any) {
const updated = await this.db.product.update({ where: { id }, data });
// All product-related caches are invalidated
return updated;
}
@CacheEvict({ keys: ['product-{id}'] })
async deleteProduct(id: string) {
return await this.db.product.delete({ where: { id } });
}
}
3. Memory Leaks
Problem: Cache grows indefinitely and consumes all memory.
Solution: Set appropriate TTLs and implement cleanup.
CacheModule.forRoot({
strategy: 'memory',
ttl: 3600, // Default 1 hour
cleanupInterval: 60000, // Cleanup every minute
maxSize: 10000, // Maximum 10,000 entries
evictionPolicy: 'lru' // Least recently used eviction
});
4. Cache Inconsistency
Problem: Cache and database are out of sync.
Solution: Use write-through for critical data.
@WriteThrough({
key: 'user-{id}',
ttl: 1800
})
async updateUser(id: string, data: any) {
// Database and cache updated together
return await this.db.user.update({ where: { id }, data });
}
5. Over-Caching
Problem: Caching data that shouldn't be cached.
Solution: Implement cache conditions and exclusions.
@Cache({
key: 'data-{id}',
ttl: 3600,
condition: (value) => value !== null && value !== undefined,
cacheNull: false
})
async getData(id: string) {
const data = await this.db.data.findUnique({ where: { id } });
return data; // Won't cache null/undefined values
}
Testing Cache Implementation
Unit Testing Strategies
// tests/services/cache.test.ts
describe('UserService Cache', () => {
let service: UserService;
let cacheService: CacheService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [UserService, CacheService],
}).compile();
service = module.get<UserService>(UserService);
cacheService = module.get<CacheService>(CacheService);
});
describe('Cache-Aside Pattern', () => {
it('should cache user data', async () => {
const userId = '123';
// First call - cache miss
const userOne = await service.getUser(userId);
expect(cacheService.get).toHaveBeenCalledWith(`user-${userId}`);
// Second call - cache hit
const userTwo = await service.getUser(userId);
expect(userOne).toEqual(userTwo);
});
it('should return fallback when user not found', async () => {
const userId = '999';
const user = await service.getUser(userId);
expect(user).toEqual({
id: 'anonymous',
name: 'Guest User',
role: 'guest'
});
});
});
describe('Cache Invalidation', () => {
it('should invalidate cache on update', async () => {
const userId = '123';
// Cache user
await service.getUser(userId);
// Update user
await service.updateUser(userId, { name: 'New Name' });
// Verify cache was invalidated
expect(cacheService.invalidateTags).toHaveBeenCalledWith(['users']);
});
});
});
Integration Testing
// tests/integration/cache.integration.test.ts
describe('Cache Integration', () => {
let app: HazelApplication;
beforeAll(async () => {
app = await Test.createTestingModule({
imports: [AppModule],
}).compile();
await app.init();
});
it('should handle distributed locking', async () => {
// Simulate concurrent requests
const promises = Array.from({ length: 5 }, (_, i) =>
fetch(`/api/reports/daily/${new Date().toISOString()}`)
);
const results = await Promise.all(promises);
// Only one should have computed the report
const computations = results.filter(r => r.headers.get('x-cache-computed') === 'true');
expect(computations).toHaveLength(1);
});
it('should maintain cache consistency', async () => {
const userId = '123';
// Get user
const getOne = await fetch(`/api/users/${userId}`);
const userOne = await getOne.json();
// Update user
await fetch(`/api/users/${userId}`, {
method: 'PUT',
body: JSON.stringify({ name: 'Updated Name' })
});
// Get user again
const getTwo = await fetch(`/api/users/${userId}`);
const userTwo = await getTwo.json();
expect(userTwo.name).toBe('Updated Name');
expect(userTwo.name).not.toBe(userOne.name);
});
});
Conclusion
Advanced caching strategies can dramatically improve your application's performance, but they require careful planning and implementation. Here are the key takeaways:
- Choose the right pattern: Match caching strategy to your use case
- Monitor continuously: Track hit rates, latency, and error rates
- Plan for failures: Implement fallbacks and error handling
- Test thoroughly: Verify cache behavior under various conditions
- Optimize iteratively: Start simple and add complexity as needed
By following these patterns and best practices, you can build highly performant, scalable applications that make effective use of caching at every level.
Next Steps
- Explore AI Package for AI response caching
- Learn about RAG Package for search result caching
- Check out Agent Package for agent state caching
- Read Deployment Guide for production caching