- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes. - Introduced configuration loading flow in configuration-management skill to clarify the configuration process. - Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers. - Enhanced various skills with additional diagrams to improve understanding of complex concepts. These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
10 KiB
name, description
| name | description |
|---|---|
| caching-patterns | Caching strategies and patterns for GoodGo microservices including multi-layer cache, Redis caching, cache key naming, TTL strategies, cache invalidation, and cache-aside patterns. |
Caching Patterns
When to Use This Skill
Use this skill when:
- Implementing caching for frequently accessed data
- Optimizing database queries with caching
- Designing cache key naming conventions
- Setting TTL (Time To Live) strategies
- Implementing cache invalidation patterns
- Using multi-layer cache (L1: Memory, L2: Redis)
- Handling cache failures gracefully
Core Concepts
Multi-Layer Cache Strategy
The platform uses a two-layer cache architecture to balance speed and capacity:
graph TB
subgraph Application["Application Layer"]
App[Application Code]
end
subgraph L1Layer["L1 Cache - Memory (NodeCache)"]
L1[In-Memory Cache]
L1Props["• Speed: < 1ms<br/>• Capacity: 10k keys<br/>• TTL: 60s-5min<br/>• Scope: Per-instance"]
end
subgraph L2Layer["L2 Cache - Redis (Distributed)"]
L2[Redis Cache]
L2Props["• Speed: < 5ms<br/>• Capacity: Large<br/>• TTL: Configurable<br/>• Scope: Shared"]
end
subgraph DataLayer["Data Source"]
DB[(Database)]
API[External API]
end
App -->|Check First| L1
L1 -->|Miss| L2
L2 -->|Miss| DB
L2 -->|Miss| API
DB -->|Store| L2
API -->|Store| L2
L2 -->|Warm| L1
L1 -.-> L1Props
L2 -.-> L2Props
style L1 fill:#e1f5ff
style L2 fill:#fff4e1
style DB fill:#ffe1e1
style API fill:#ffe1e1
Layer Characteristics:
-
L1 Cache (Memory): NodeCache in-memory cache
- Very fast (< 1ms access time)
- Limited capacity (10k keys default)
- Short TTL (60 seconds default, max 5 minutes)
- Per-instance (not shared across instances)
-
L2 Cache (Redis): Distributed Redis cache
- Fast (< 5ms access time)
- Large capacity
- Longer TTL (configurable)
- Shared across all service instances
Cache Flow
The cache lookup follows a multi-layer approach, checking L1 first, then L2, and finally the data source.
flowchart TD
Start([Request Data]) --> CheckL1{Check L1 Cache<br/>Memory}
CheckL1 -->|Hit| ReturnL1[Return Data<br/>from L1]
CheckL1 -->|Miss| CheckL2{Check L2 Cache<br/>Redis}
CheckL2 -->|Hit| StoreL1[Store in L1<br/>Warm Cache]
StoreL1 --> ReturnL2[Return Data<br/>from L2]
CheckL2 -->|Miss| FetchSource[Fetch from<br/>Data Source]
FetchSource --> StoreBoth[Store in L1 & L2]
StoreBoth --> ReturnSource[Return Data<br/>from Source]
ReturnL1 --> End([End])
ReturnL2 --> End
ReturnSource --> End
style CheckL1 fill:#e1f5ff
style CheckL2 fill:#fff4e1
style FetchSource fill:#ffe1e1
style ReturnL1 fill:#e1ffe1
style ReturnL2 fill:#e1ffe1
style ReturnSource fill:#e1ffe1
Patterns
Cache Service Usage
import { cacheService } from '../core/cache';
// Simple get/set
const cached = await cacheService.get<User>('user:123');
await cacheService.set('user:123', userData, 300); // 5 minutes TTL
// Get or set pattern (cache-aside)
const user = await cacheService.getOrSet(
'user:123',
async () => {
return await userRepository.findById('123');
},
300 // TTL in seconds
);
Cache Key Naming Conventions
Use consistent naming patterns:
// Pattern: {entity}:{identifier}
'user:123'
'user:email:user@example.com'
'user:123:permissions'
'user:123:roles'
// Pattern: {entity}:{identifier}:{sub-resource}
'session:abc123'
'permission:perm_123'
'role:role_123'
Cache service provides key generators:
cacheService.keys = {
user: (userId: string) => `user:${userId}`,
userPermissions: (userId: string) => `user:${userId}:permissions`,
userRoles: (userId: string) => `user:${userId}:roles`,
token: (token: string) => `token:${token}`,
session: (sessionId: string) => `session:${sessionId}`,
};
Cache-Aside Pattern
Most common pattern - check cache first, fetch if miss:
async getUserPermissions(userId: string): Promise<string[]> {
const cacheKey = cacheService.keys.userPermissions(userId);
// Try cache first
const cached = await cacheService.get<string[]>(cacheKey);
if (cached) {
return cached;
}
// Cache miss - fetch from source
const permissions = await calculatePermissions(userId);
// Store in cache
await cacheService.set(cacheKey, permissions, 300); // 5 min TTL
return permissions;
}
Get or Set Pattern
Simplified cache-aside pattern:
const permissions = await cacheService.getOrSet(
cacheService.keys.userPermissions(userId),
async () => {
// This function only runs on cache miss
return await calculatePermissions(userId);
},
300 // TTL
);
TTL Strategies
Choose TTL based on data characteristics:
Short TTL (60-300s): Frequently changing data
- User permissions (300s)
- Session data (varies)
- Real-time statistics
Medium TTL (300-1800s): Moderately changing data
- User profiles (600s)
- Organization data (900s)
- Configuration (1800s)
Long TTL (1800-3600s): Rarely changing data
- Static configurations (3600s)
- Reference data (7200s)
No TTL: Very stable data (use with caution)
- Rarely use - prefer long TTL instead
Cache Invalidation
Invalidate cache when data changes to prevent serving stale data. The platform supports multiple invalidation strategies:
flowchart TD
Start([Data Changed]) --> ChooseStrategy{Choose<br/>Invalidation<br/>Strategy}
ChooseStrategy -->|Single Key| SingleKey[Single Key<br/>Invalidation]
SingleKey --> DelL1[Delete from L1]
DelL1 --> DelL2[Delete from L2]
DelL2 --> Done1([Complete])
ChooseStrategy -->|Pattern Match| PatternMatch[Pattern-Based<br/>Invalidation]
PatternMatch --> FindKeys[Find Matching Keys<br/>user:123:*]
FindKeys --> DelManyL1[Delete from L1<br/>All Matching]
DelManyL1 --> DelManyL2[Delete from L2<br/>All Matching]
DelManyL2 --> Done2([Complete])
ChooseStrategy -->|Multiple Keys| MultipleKeys[Multiple Keys<br/>Invalidation]
MultipleKeys --> ListKeys[List Keys to Delete<br/>user:123<br/>user:123:permissions<br/>user:123:roles]
ListKeys --> BatchDelL1[Batch Delete from L1]
BatchDelL1 --> BatchDelL2[Batch Delete from L2]
BatchDelL2 --> Done3([Complete])
style SingleKey fill:#e1f5ff
style PatternMatch fill:#fff4e1
style MultipleKeys fill:#ffe1e1
style Done1 fill:#e1ffe1
style Done2 fill:#e1ffe1
style Done3 fill:#e1ffe1
Implementation Examples:
// Single key invalidation
await cacheService.del(cacheService.keys.user(userId));
await cacheService.del(cacheService.keys.userPermissions(userId));
// Pattern-based invalidation
await cacheService.invalidatePattern('user:123:*');
// Multiple keys
await cacheService.delMany([
cacheService.keys.user(userId),
cacheService.keys.userPermissions(userId),
cacheService.keys.userRoles(userId),
]);
Cache Warming
Pre-populate cache with frequently accessed data:
async warmCache() {
const activeUsers = await userRepository.findActiveUsers();
for (const user of activeUsers) {
// Pre-cache user data
await cacheService.set(
cacheService.keys.user(user.id),
user,
600
);
// Pre-cache permissions
const permissions = await calculatePermissions(user.id);
await cacheService.set(
cacheService.keys.userPermissions(user.id),
permissions,
300
);
}
}
Error Handling
Cache failures should not break the application:
async getWithCache(key: string): Promise<Data | null> {
try {
// Try cache first
const cached = await cacheService.get<Data>(key);
if (cached) return cached;
} catch (error) {
// Log but continue - fallback to source
logger.warn('Cache get failed, falling back to source', { key, error });
}
// Fallback to data source
return await fetchFromSource();
}
Best Practices
- Cache Keys: Use consistent naming conventions
- TTL Selection: Choose TTL based on data change frequency
- Invalidation: Invalidate cache when data changes
- Error Handling: Don't let cache failures break the app
- Cache-Aside: Use cache-aside pattern for most cases
- Avoid Over-Caching: Don't cache data that changes too frequently
- Monitor Hit Rates: Track cache hit rates to optimize TTL
- Warm Cache: Pre-populate cache for critical data
- Use Multi-Layer: Leverage both L1 and L2 cache
- Serialize Properly: Ensure data is JSON serializable
Common Mistakes
- Cache Key Collisions: Using generic keys that collide
- Stale Data: Not invalidating cache when data changes
- Too Short TTL: Setting TTL too short, negating cache benefits
- Too Long TTL: Setting TTL too long, serving stale data
- No Error Handling: Letting cache errors break the application
- Caching Everything: Caching data that doesn't benefit from caching
- Not Warming Cache: Not pre-populating critical cache data
- Ignoring Hit Rates: Not monitoring cache performance
Troubleshooting
Low Cache Hit Rate
Problem: Cache hit rate is low, cache not effective Solution:
- Review TTL values - may be too short
- Check cache key patterns - ensure consistent usage
- Verify cache invalidation isn't too aggressive
- Monitor what data is being cached
Stale Data Issues
Problem: Serving stale data from cache Solution:
- Review TTL values - may be too long
- Ensure cache invalidation on data updates
- Use shorter TTL for frequently changing data
- Implement cache versioning if needed
Cache Performance Issues
Problem: Cache operations are slow Solution:
- Check Redis connection and network latency
- Monitor Redis memory usage
- Review cache key patterns for efficiency
- Consider L1 cache hit rate (should be high)
Resources
- Multi-Layer Cache - Multi-layer cache implementation
- Cache Service - Cache service wrapper
- Cache Usage Example - Real-world cache usage