Caching System
Learn how to leverage the powerful Redis caching layer for ultra-fast API responses.
Overviewβ
Express Base API includes a sophisticated caching system that:
- β‘ Reduces response time by 50-100x
- π Dual backend - Redis (primary) with in-memory fallback
- π― Automatic invalidation on data changes
- π Built-in statistics to monitor performance
- π‘οΈ Type-safe with full TypeScript support
Architectureβ
Request β Cache Check β Cache Hit? β Response (< 1ms)
β
Cache Miss
β
Database Query β Cache Result β Response
Basic Usageβ
In Servicesβ
All services extending BaseService have caching methods available:
import BaseService from '../../../utils/bases/base.service';
class ProductService extends BaseService<IProduct> {
async getProduct(id: string) {
// Find by ID with automatic caching (1 hour TTL)
return await this.findByIdCached(id, {}, null, 3600);
}
async getFeaturedProducts() {
// Cache-aside pattern
return await this.cacheGetOrSet(
'products:featured',
async () => {
return await this.find(
{ featured: true } as any,
{ sort: { views: -1 }, limit: 10 }
);
},
3600 // 1 hour TTL
);
}
}
Manual Cachingβ
// Set cache
await service.cacheSet('user:123', userData, 3600); // 1 hour
// Get from cache
const user = await service.cacheGet('user:123');
// Delete from cache
await service.cacheDelete('user:123');
// Delete by pattern
await service.cacheDeletePattern('user:*');
Cache Methods Referenceβ
Protected Methods (available in services)β
cacheGet<T>(key: string): Promise<T | null>β
Get value from cache.
const product = await this.cacheGet<IProduct>('product:123');
if (product) {
return { error: false, data: product };
}
cacheSet(key: string, value: any, ttl: number): Promise<boolean>β
Store value in cache with TTL (Time-To-Live) in seconds.
await this.cacheSet('product:123', productData, 3600); // 1 hour
cacheDelete(key: string): Promise<boolean>β
Remove specific cache entry.
await this.cacheDelete('product:123');
cacheDeletePattern(pattern: string): Promise<number>β
Remove multiple entries matching a pattern.
const deleted = await this.cacheDeletePattern('product:*');
console.log(`Deleted ${deleted} cache entries`);
cacheGetOrSet<T>(key: string, fetcher: Function, ttl: number): Promise<T | null>β
Cache-aside pattern - get from cache or execute fetcher and cache result.
const products = await this.cacheGetOrSet(
'products:category:electronics',
async () => await this.find({ category: 'electronics' } as any),
3600
);
Public Methodsβ
findByIdCached(id, query, model, ttl): Promise<QueryResult<T>>β
Find by ID with automatic caching.
const result = await productService.findByIdCached(
'123',
{ populate: 'category' },
null,
3600
);
invalidateCache(id, model): Promise<boolean>β
Invalidate cache for a specific document.
await productService.update(productId, updateData);
await productService.invalidateCache(productId);
invalidateAllCache(model): Promise<number>β
Invalidate all cache entries for a model.
const deleted = await productService.invalidateAllCache();
Authentication Cachingβ
The authentication system automatically caches user data for blazing-fast subsequent requests.
How It Worksβ
- On Login - User + Auth data cached (1-hour TTL)
- On Authentication - Middleware checks cache first (< 1ms)
- On Logout/Password Reset - Cache automatically invalidated
Performance Impactβ
| Operation | Without Cache | With Cache | Improvement |
|---|---|---|---|
| Auth check | 50-100ms | < 1ms | 50-100x faster |
| API request | DB query each time | Cache hit | Massive DB load reduction |
Example Flowβ
// 1. User logs in
POST /auth/login
// β User data cached for 1 hour (key: user:auth:{userId})
// 2. Subsequent authenticated requests
GET /api/profile
// β Middleware checks cache (< 1ms)
// β No DB query needed! β
// 3. User logs out
POST /auth/logout
// β Cache invalidated
// β Next request requires DB query
Cache Statisticsβ
Monitor cache performance:
import cacheService from './utils/helpers/cache.helper';
const stats = cacheService.getStats();
console.log(stats);
Output:
{
"hits": 1500,
"misses": 250,
"sets": 250,
"deletes": 50,
"errors": 0,
"hitRate": "85.71%",
"backend": "redis",
"size": undefined
}
Reset Statisticsβ
cacheService.resetStats();
Best Practicesβ
1. Choose Appropriate TTLβ
// Short-lived data (user session)
await this.cacheSet('session:123', session, 3600); // 1 hour
// Medium-lived data (product listings)
await this.cacheSet('products:featured', products, 86400); // 1 day
// Long-lived data (static content)
await this.cacheSet('config:site', config, 604800); // 1 week
2. Use Descriptive Keysβ
// β
Good - Clear and hierarchical
'user:123'
'user:123:profile'
'products:category:electronics'
'products:featured:2024-01-15'
// β Bad - Unclear and hard to invalidate
'u123'
'temp_data'
'cache_12345'
3. Invalidate on Updatesβ
async updateProduct(id: string, data: any) {
// Update database
const result = await this.update({ _id: id } as any, data);
if (!result.error) {
// Invalidate specific cache
await this.invalidateCache(id);
// Invalidate related caches
await this.cacheDelete('products:featured');
await this.cacheDeletePattern(`products:category:*`);
}
return result;
}
4. Handle Cache Miss Gracefullyβ
async getProduct(id: string) {
// Try cache first
const cached = await this.cacheGet<IProduct>(`product:${id}`);
if (cached) {
return { error: false, data: cached };
}
// Cache miss - query database
const result = await this.findById(id);
// Cache for next time
if (!result.error && result.data) {
await this.cacheSet(`product:${id}`, result.data, 3600);
}
return result;
}
Advanced Patternsβ
Cache Warmingβ
Pre-populate cache with frequently accessed data:
async warmCache() {
// Get most popular products
const popular = await this.find(
{} as any,
{ sort: { views: -1 }, limit: 100 }
);
// Cache each product
if (!popular.error && popular.data) {
for (const product of popular.data) {
await this.cacheSet(
`product:${product._id}`,
product,
3600
);
}
}
}
Conditional Cachingβ
Cache based on conditions:
async getProducts(filters: any, useCache: boolean = true) {
const cacheKey = `products:${JSON.stringify(filters)}`;
if (useCache) {
const cached = await this.cacheGet(cacheKey);
if (cached) return { error: false, data: cached };
}
const result = await this.find(filters);
if (useCache && !result.error) {
await this.cacheSet(cacheKey, result.data, 3600);
}
return result;
}
Cache Tagsβ
Invalidate multiple related caches:
async createProduct(data: any) {
const result = await this.create(data);
if (!result.error) {
// Invalidate all product-related caches
await this.cacheDeletePattern('products:*');
await this.cacheDeletePattern(`category:${data.category}:*`);
}
return result;
}
Troubleshootingβ
Cache Not Workingβ
-
Check Redis connection:
redis-cli ping
# Should respond: PONG -
Verify configuration:
# In .env
USE_QUEUE=yes
REDIS_HOST=localhost
REDIS_PORT=6379 -
Check logs:
β Cache service using Redis
# or
β οΈ Cache service falling back to in-memory cache
Low Hit Rateβ
- TTL too short - increase cache duration
- Keys changing too often - use stable cache keys
- Cache invalidated too aggressively - review invalidation logic
Memory Issuesβ
Monitor Redis memory:
redis-cli INFO memory
Set max memory policy in Redis config:
maxmemory 256mb
maxmemory-policy allkeys-lru
Configurationβ
Redis Configurationβ
const redisOptions = {
host: configs.getValue('REDIS_HOST'),
port: Number(configs.getValue('REDIS_PORT')),
password: configs.getValue('REDIS_PASSWORD', false),
maxRetriesPerRequest: 3,
retryStrategy: (times) => Math.min(times * 50, 2000),
};
Cache Service Configurationβ
Default TTL and other settings can be customized in utils/helpers/cache.helper.ts.
Next Stepsβ
- Performance Optimization - Advanced caching strategies
- Login History - Automatic session caching
- Rate Limiting - Redis-based rate limiting
Questions? Check the GitHub Discussions or open an issue.