Files
pos-system/.agent/rules/caching-patterns.md

8.2 KiB

trigger
trigger
always_on

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:

  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

Request → L1 Cache → Hit? Return
           ↓ Miss
         L2 Cache → Hit? Return + Warm L1
           ↓ Miss
         Data Source (DB/API) → Store in L1 & L2 → Return

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:

// 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)

Quick Reference

Cache Layer Speed Capacity TTL Scope
L1 (Memory) <1ms 10k keys 60s-5min Per instance
L2 (Redis) <5ms Large Configurable Shared

TTL Guidelines:

Data Type TTL Example
Session data 15-60min User sessions
Permissions 5min RBAC cache
User profiles 10min Profile data
Config 30min Feature flags
Static data 1-2hr Reference data

Key Patterns:

// Entity
`user:${userId}`
`session:${sessionId}`

// Entity + Sub-resource
`user:${userId}:permissions`
`user:${userId}:roles`

// List/Collection
`users:list:page:${page}`
`products:category:${categoryId}`

Essential Operations:

// Get/Set
await cache.get<T>(key);
await cache.set(key, value, ttl);

// Get or fetch
await cache.getOrSet(key, fetchFn, ttl);

// Invalidate
await cache.del(key);
await cache.invalidatePattern('user:123:*');

Resources