- 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.
370 lines
10 KiB
Markdown
370 lines
10 KiB
Markdown
---
|
|
name: caching-patterns
|
|
description: 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:
|
|
|
|
```mermaid
|
|
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:**
|
|
|
|
1. **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)
|
|
|
|
2. **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.
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```mermaid
|
|
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:**
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
1. **Cache Keys**: Use consistent naming conventions
|
|
2. **TTL Selection**: Choose TTL based on data change frequency
|
|
3. **Invalidation**: Invalidate cache when data changes
|
|
4. **Error Handling**: Don't let cache failures break the app
|
|
5. **Cache-Aside**: Use cache-aside pattern for most cases
|
|
6. **Avoid Over-Caching**: Don't cache data that changes too frequently
|
|
7. **Monitor Hit Rates**: Track cache hit rates to optimize TTL
|
|
8. **Warm Cache**: Pre-populate cache for critical data
|
|
9. **Use Multi-Layer**: Leverage both L1 and L2 cache
|
|
10. **Serialize Properly**: Ensure data is JSON serializable
|
|
|
|
## Common Mistakes
|
|
|
|
1. **Cache Key Collisions**: Using generic keys that collide
|
|
2. **Stale Data**: Not invalidating cache when data changes
|
|
3. **Too Short TTL**: Setting TTL too short, negating cache benefits
|
|
4. **Too Long TTL**: Setting TTL too long, serving stale data
|
|
5. **No Error Handling**: Letting cache errors break the application
|
|
6. **Caching Everything**: Caching data that doesn't benefit from caching
|
|
7. **Not Warming Cache**: Not pre-populating critical cache data
|
|
8. **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](../../services/iam-service/src/core/cache/multi-layer-cache.ts) - Multi-layer cache implementation
|
|
- [Cache Service](../../services/iam-service/src/core/cache/cache.service.ts) - Cache service wrapper
|
|
- [Cache Usage Example](../../services/iam-service/src/modules/rbac/rbac.service.ts) - Real-world cache usage
|