Cache Package
The @hazeljs/cache package provides powerful caching capabilities with support for multiple strategies including memory, Redis, and multi-tier caching. It includes decorators for easy method-level caching and tag-based invalidation.
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:
Key Components
- CacheService: Main service providing unified cache operations
- Cache Strategies: Pluggable implementations (Memory, Redis, Multi-Tier)
- Decorators:
@Cache,@CacheKey,@CacheTTL,@CacheTags,@CacheEvict - 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 { Injectable } from '@hazeljs/core';
import { CacheService } from '@hazeljs/cache';
@Injectable()
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:
@Injectable()
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)
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:
@Injectable()
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 { Injectable } from '@hazeljs/core';
import { Controller, Get, Post, Put, Delete, Param, Body } from '@hazeljs/core';
import { CacheService, Cache, CacheEvict, CacheTags } from '@hazeljs/cache';
@Injectable()
export class ProductService {
constructor(private readonly cache: CacheService) {}
async findOne(id: string) {
return await this.cache.getOrSet(
`product:${id}`,
async () => await this.db.product.findUnique({ where: { id } }),
3600,
['products']
);
}
async create(data: CreateProductDto) {
const product = await this.db.product.create({ data });
await this.cache.invalidateTags(['products']);
return product;
}
}
@Controller('products')
export class ProductsController {
constructor(private readonly productService: ProductService) {}
@Get(':id')
@Cache({ ttl: 3600, tags: ['products'] })
async getProduct(@Param('id') id: string) {
return await this.productService.findOne(id);
}
@Post()
@CacheEvict({ tags: ['products'] })
async createProduct(@Body() createProductDto: CreateProductDto) {
return await this.productService.create(createProductDto);
}
@Put(':id')
@CacheEvict({ keys: ['product:{id}'], tags: ['products'] })
async updateProduct(
@Param('id') id: string,
@Body() updateProductDto: UpdateProductDto
) {
return await this.productService.update(id, updateProductDto);
}
}
Best Practices
-
Choose the right strategy: Use memory for single-instance apps, Redis for distributed systems.
-
Set appropriate TTLs: Balance between freshness and performance.
-
Use tags for invalidation: Tag related cache entries for easy bulk invalidation.
-
Cache warming: Pre-populate cache for frequently accessed data.
-
Monitor cache stats: Track hit rates to optimize cache configuration.