Files
pos-system/docs/en/skills/caching-patterns.md
Ho Ngoc Hai 2640b351c3 Enhance documentation with detailed diagrams and structured flows
- 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.
2026-01-01 23:22:54 +07:00

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:

  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.

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

  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