Caching
Redis-based caching system for improving performance by reducing database queries and speeding up frequently accessed data.
Overview
- Redis for in-memory caching
- Singleton pattern for connection management
- JSON serialization for complex objects
- TTL support for automatic expiration
- Cache invalidation strategies
Redis Setup
Connection Configuration
// adapters/shared/cache/redis.ts
import { createClient, RedisClientType } from '@redis/client';
export class RedisCache implements AbstractCache {
private static instance: RedisCache;
private client: RedisClientType;
static getInstance() {
if (!RedisCache.instance) {
RedisCache.instance = new RedisCache();
const client = createClient({
url: process.env.REDIS_URL,
});
client
.on('error', (err) => logger.error(`Redis Client Error: ${err}`))
.on('connect', () => logger.info('Redis connected'))
.connect();
RedisCache.instance.client = client;
}
return RedisCache.instance;
}
async get(key: string) {
const value = await this.client.get(key);
return value ? JSON.parse(value) : null;
}
async set(key: string, value: any, ttl?: number) {
const stringValue = JSON.stringify(value);
if (ttl) {
await this.client.setEx(key, ttl, stringValue);
} else {
await this.client.set(key, stringValue);
}
}
async delete(key: string) {
await this.client.del(key);
}
async clear() {
await this.client.flushAll();
}
}
Environment Variables
# Redis
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=your-redis-password
REDIS_DB=0
# Cache TTL (in seconds)
CACHE_TTL_SHORT=300 # 5 minutes
CACHE_TTL_MEDIUM=1800 # 30 minutes
CACHE_TTL_LONG=3600 # 1 hour
Cache Interface
// app/shared/cache.ts
export abstract class AbstractCache {
abstract get(key: string): Promise<any>;
abstract set(key: string, value: any, ttl?: number): Promise<void>;
abstract delete(key: string): Promise<void>;
abstract clear(): Promise<void>;
}
Usage Patterns
Basic Caching
const cache = RedisCache.getInstance();
// Set value
await cache.set('user:123', userData);
// Get value
const user = await cache.get('user:123');
// Delete value
await cache.delete('user:123');
Cache with TTL
// Cache for 5 minutes
await cache.set('trending:services', services, 300);
// Cache for 1 hour
await cache.set('categories:all', categories, 3600);
Cache-Aside Pattern
async function getUser(userId: string) {
const cacheKey = `user:${userId}`;
// Try cache first
let user = await cache.get(cacheKey);
if (!user) {
// Cache miss - fetch from database
user = await userRepository.get(userId);
// Store in cache
await cache.set(cacheKey, user, 1800); // 30 min TTL
}
return user;
}
Cache Invalidation
// Invalidate on update
async function updateUser(userId: string, data: any) {
const user = await userRepository.update(userId, data);
// Invalidate cache
await cache.delete(`user:${userId}`);
return user;
}
// Invalidate related caches
async function updateService(serviceId: string, data: any) {
const service = await serviceRepository.update(serviceId, data);
// Invalidate multiple related caches
await cache.delete(`service:${serviceId}`);
await cache.delete(`services:owner:${service.ownerId}`);
await cache.delete(`services:category:${service.categoryId}`);
return service;
}
Caching Strategies
1. User Data
// Cache user profile
const cacheKey = `user:${userId}`;
await cache.set(cacheKey, user, 1800); // 30 minutes
// Cache user permissions
const permKey = `user:${userId}:permissions`;
await cache.set(permKey, permissions, 3600); // 1 hour
2. Service Catalog
// Cache active services list
const servicesKey = 'services:active';
await cache.set(servicesKey, services, 300); // 5 minutes
// Cache service details
const serviceKey = `service:${serviceId}`;
await cache.set(serviceKey, service, 1800); // 30 minutes
3. Categories
// Cache category tree (rarely changes)
const categoriesKey = 'categories:all';
await cache.set(categoriesKey, categories, 7200); // 2 hours
4. Trending/Popular Data
// Cache trending services (updates frequently)
const trendingKey = 'services:trending';
await cache.set(trendingKey, trending, 600); // 10 minutes
// Cache popular searches
const searchKey = `search:${query}`;
await cache.set(searchKey, results, 1800); // 30 minutes
5. Session Data
// Cache active sessions
const sessionKey = `session:${userId}`;
await cache.set(sessionKey, sessionData, 86400); // 24 hours
Cache Key Patterns
Naming Convention
// Entity-based keys
`user:${userId}`
`service:${serviceId}`
`order:${orderId}`
// Collection keys
`services:active`
`services:category:${categoryId}`
`services:owner:${ownerId}`
// Query result keys
`search:${query}`
`trending:services`
`popular:categories`
// Session keys
`session:${userId}`
`auth:token:${tokenHash}`
Key Namespace Organization
user:123 # User entity
user:123:permissions # User permissions
user:123:services # User's services
service:456 # Service entity
service:456:options # Service options
services:active # Active services list
services:category:789 # Services in category
search:logo design # Search results
trending:services # Trending services
session:user:123 # User session
Cache Warming
Pre-populate cache with frequently accessed data:
async function warmCache() {
logger.info('Warming up cache...');
// Cache active services
const services = await serviceRepository.findAll({ status: 'active' });
await cache.set('services:active', services, 300);
// Cache categories
const categories = await categoryRepository.findAll();
await cache.set('categories:all', categories, 7200);
// Cache trending services
const trending = await serviceRepository.getTrending(10);
await cache.set('services:trending', trending, 600);
logger.info('Cache warming complete');
}
// Run on application startup
warmCache();
Cache Monitoring
Cache Hit Rate
let cacheHits = 0;
let cacheMisses = 0;
async function getCached(key: string, fetchFn: () => Promise<any>) {
const cached = await cache.get(key);
if (cached) {
cacheHits++;
return cached;
}
cacheMisses++;
const data = await fetchFn();
await cache.set(key, data);
return data;
}
// Calculate hit rate
function getCacheHitRate() {
const total = cacheHits + cacheMisses;
return total > 0 ? (cacheHits / total) * 100 : 0;
}
Cache Size Monitoring
async function getCacheStats() {
const info = await cache.client.info('memory');
const keys = await cache.client.dbSize();
return {
memoryUsed: parseMemoryUsage(info),
totalKeys: keys,
hitRate: getCacheHitRate(),
};
}
Best Practices
✅ Do
- Use appropriate TTL – Short TTL for volatile data, long for stable data
- Invalidate on updates – Clear cache when data changes
- Use consistent key naming – Follow namespace convention
- Handle cache failures – Fallback to database if cache unavailable
- Monitor hit rates – Track cache effectiveness
- Serialize consistently – Use JSON for complex objects
- Set reasonable limits – Don't cache everything
❌ Don't
- Don't cache user-specific data indefinitely – Use sessions or short TTL
- Don't forget to invalidate – Stale cache is worse than no cache
- Don't cache large objects – Redis has memory limits
- Don't use cache as primary storage – It's volatile
- Don't cache sensitive data – Or encrypt it
- Don't over-complicate keys – Keep them simple and readable
Redis Configuration
redis.conf
# Memory
maxmemory 256mb
maxmemory-policy allkeys-lru # Evict least recently used keys
# Persistence (optional)
save 900 1 # Save after 900 sec if at least 1 key changed
save 300 10 # Save after 300 sec if at least 10 keys changed
save 60 10000 # Save after 60 sec if at least 10000 keys changed
# Security
requirepass your-redis-password
# Network
bind 127.0.0.1
port 6379
# Logging
loglevel notice
logfile /var/log/redis/redis-server.log
Testing
Mock Cache
// tests/mocks/cache.mock.ts
export class MockCache implements AbstractCache {
private store = new Map<string, any>();
async get(key: string) {
return this.store.get(key) || null;
}
async set(key: string, value: any) {
this.store.set(key, value);
}
async delete(key: string) {
this.store.delete(key);
}
async clear() {
this.store.clear();
}
}
Cache Tests
describe('Cache', () => {
let cache: RedisCache;
beforeAll(() => {
cache = RedisCache.getInstance();
});
afterEach(async () => {
await cache.clear();
});
it('should set and get value', async () => {
await cache.set('test:key', { data: 'value' });
const result = await cache.get('test:key');
expect(result).toEqual({ data: 'value' });
});
it('should delete value', async () => {
await cache.set('test:key', 'value');
await cache.delete('test:key');
const result = await cache.get('test:key');
expect(result).toBeNull();
});
it('should expire with TTL', async () => {
await cache.set('test:key', 'value', 1); // 1 second TTL
await sleep(1500);
const result = await cache.get('test:key');
expect(result).toBeNull();
});
});
Summary
The caching system provides:
- Redis integration for fast in-memory storage
- Singleton pattern for connection management
- Flexible TTL for different data types
- Cache invalidation strategies
- Consistent key naming patterns
- Performance monitoring capabilities
- Fallback handling for reliability
This caching layer significantly improves application performance by reducing database load and speeding up frequently accessed data.