Cache Implementation Guide
Overview
Clean Stack provides a flexible caching system through the @clean-stack/cache
package. The implementation supports multiple cache providers with a consistent interface.
Core Components
Installation
bun install @clean-stack/cache
Basic Usage
import { createCacheStore } from '@clean-stack/cache';
import { createRedisProvider } from '@clean-stack/redis';
// Initialize provider
const redisProvider = await createRedisProvider({
url: 'redis://localhost:6379'
});
// Create cache store
const cacheStore = await createCacheStore(redisProvider);
// Basic operations
await cacheStore.set('key', 'value', { ttl: 3600 });
const value = await cacheStore.get('key');
await cacheStore.delete('key');
Cache Provider Interface
interface CacheProvider {
set(key: string, value: string, ttl?: number): Promise<void>;
get(key: string): Promise<string | null>;
delete(key: string): Promise<void>;
deleteManyKeys(keys: string[]): Promise<void>;
clear(): Promise<void>;
getAllKeys(): Promise<string[]>;
}
Invalidation Groups
Groups allow efficient invalidation of related cache entries:
// Store with groups
await cacheStore.set('user:1', userData, {
groups: ['users', 'active-users']
});
await cacheStore.set('user:2', userData2, {
groups: ['users', 'inactive-users']
});
// Invalidate by group
await cacheStore.invalidateGroup('active-users'); // Removes user:1
await cacheStore.invalidateGroup('users'); // Removes both
Middleware Usage
Integrate with Koa middleware for automatic caching:
import { createCacheMiddleware } from '@clean-stack/cache';
const cacheMiddleware = createCacheMiddleware(cacheStore, {
defaultTTL: 3600,
keyPrefix: 'api:',
});
router.get('/users/:id',
cacheMiddleware(), // Cache with default options
async (ctx) => {
// Handler logic
}
);
router.get('/products',
cacheMiddleware({
ttl: 1800,
keyGenerator: (ctx) => `products:${ctx.query.category}`,
groups: ['products']
}),
async (ctx) => {
// Handler logic
}
);
Performance Considerations
-
Key Design
- Use consistent naming patterns
- Include version in keys if data format changes
- Keep keys reasonably short
-
TTL Strategy
- Set appropriate TTLs based on data volatility
- Use shorter TTLs for frequently changing data
- Consider using infinite TTL for static data
-
Group Management
- Group related items logically
- Don't overuse groups - they add overhead
- Consider data relationships when designing groups
Monitoring & Statistics
The cache store tracks important metrics:
const stats = await cacheStore.getStatistics();
console.log(stats);
// {
// hits: 150,
// misses: 45,
// hitRate: 0.769,
// size: 195,
// groupCount: 5
// }
Error Handling
The cache store includes built-in error handling:
try {
await cacheStore.set('key', 'value');
} catch (error) {
if (error instanceof CacheProviderError) {
// Handle provider-specific errors
} else if (error instanceof CacheKeyError) {
// Handle key validation errors
}
}
Best Practices
-
Data Serialization
// Do serialize complex data
await cache.set('user', JSON.stringify(user));
const user = JSON.parse(await cache.get('user')); -
Error Handling
// Handle cache failures gracefully
const getUserData = async (id: string) => {
try {
const cached = await cache.get(`user:${id}`);
if (cached) return JSON.parse(cached);
} catch (error) {
logger.warn('Cache error', error);
}
return fetchUserFromDB(id);
}; -
Group Management
// Group related data
await cache.set(`user:${id}`, userData, {
groups: ['users', `user:${id}:*`]
});
await cache.set(`user:${id}:preferences`, prefs, {
groups: [`user:${id}:*`]
});
Common Patterns
Cache-Aside Pattern
async function getUser(id: string) {
const cacheKey = `user:${id}`;
// Try cache first
const cached = await cacheStore.get(cacheKey);
if (cached) return JSON.parse(cached);
// Cache miss - get from database
const user = await database.users.findById(id);
// Store in cache
await cacheStore.set(cacheKey, JSON.stringify(user), {
ttl: 3600,
groups: ['users']
});
return user;
}
Bulk Operations
async function bulkGetUsers(ids: string[]) {
const cacheKeys = ids.map(id => `user:${id}`);
// Get all cached users
const cachedUsers = await Promise.all(
cacheKeys.map(key => cacheStore.get(key))
);
// Find missing users
const missingIds = ids.filter((_, i) => !cachedUsers[i]);
if (missingIds.length > 0) {
const dbUsers = await database.users.findByIds(missingIds);
// Cache missing users
await Promise.all(
dbUsers.map(user =>
cacheStore.set(`user:${user.id}`, JSON.stringify(user), {
groups: ['users']
})
)
);
// Merge results
return ids.map((id, i) =>
cachedUsers[i] ? JSON.parse(cachedUsers[i]) :
dbUsers.find(u => u.id === id)
);
}
return cachedUsers.map(u => JSON.parse(u));
}