DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Caching Strategies with Claude Code: Redis Patterns and Cache Invalidation

Caching is easy to get wrong. Cache too aggressively and you serve stale data. Cache too little and you get no benefit. Claude Code can implement caching patterns correctly — with the right CLAUDE.md configuration.


CLAUDE.md for Caching

## Caching Rules

### What to Cache
- Master data (user roles, config, feature flags): TTL 5min
- Expensive computations (aggregations, reports): TTL 15min
- User sessions: TTL = session expiry
- API responses from external services: TTL 1min

### What NOT to Cache
- User-specific transactional data (orders, payments in flight)
- Real-time data (stock prices, live notifications)
- Anything that must be consistent immediately after write

### Cache Keys
- Format: `{service}:{entity}:{id}:{version}` or `{service}:{entity}:list:{filter_hash}`
- Examples: `user:profile:123`, `product:list:abc123`, `config:features:v1`
- Never use raw user input in cache keys (injection risk)

### Invalidation Strategy
- Write-through: update cache on every write (for consistency)
- TTL-based: use short TTL when consistency is flexible
- Event-based: invalidate on domain events (prefer this for critical data)

### Redis Configuration
- Client: src/lib/redis.ts (singleton)
- Default serialization: JSON
- Error handling: cache miss on Redis error (never fail the request)
Enter fullscreen mode Exit fullscreen mode

Redis Cache Wrapper

Generate a Redis cache utility module.
Requirements:
- get(key: string): return parsed JSON or null on miss/error
- set(key: string, value: unknown, ttlSeconds: number): set with expiry
- del(key: string | string[]): delete one or multiple keys
- exists(key: string): boolean check
- Never throw on Redis errors  return null/false instead
- Log Redis errors to logger.ts but don't crash

Location: src/lib/cache.ts
Enter fullscreen mode Exit fullscreen mode

Cache-Aside Pattern

Implement the cache-aside pattern for getUserProfile():
1. Check Redis for cached profile
2. On hit: return cached data
3. On miss: fetch from DB, store in Redis (TTL 5min), return
4. On Redis error: fetch from DB without caching (graceful degradation)

Cache key format: user:profile:{userId}

[paste the existing getUserProfile function]
Enter fullscreen mode Exit fullscreen mode

Generated pattern:

async function getUserProfile(userId: string): Promise<UserProfile | null> {
  const cacheKey = `user:profile:${userId}`;

  // Try cache first
  const cached = await cache.get<UserProfile>(cacheKey);
  if (cached) return cached;

  // Fetch from DB
  const profile = await db.user.findUnique({ where: { id: userId } });
  if (!profile) return null;

  // Store in cache (don't await — non-blocking)
  cache.set(cacheKey, profile, 300).catch(logger.error);

  return profile;
}
Enter fullscreen mode Exit fullscreen mode

Cache Invalidation

Generate cache invalidation logic for when a user updates their profile.
Requirements:
- Invalidate user:profile:{userId}
- Invalidate any list caches that include this user
- Use a domain event pattern (not direct cache calls in the service)

Pattern: service emits 'user.updated' event  cache handler invalidates

[paste the user update service code]
Enter fullscreen mode Exit fullscreen mode

Read-Through for Lists

Implement paginated user list caching with cursor-based pagination.
Requirements:
- Cache key includes the cursor and limit: user:list:{cursor}:{limit}
- TTL: 2 minutes (shorter due to mutation risk)
- Invalidate all user:list:* keys when any user is created/deleted
- Use Redis SCAN for bulk invalidation (not KEYS  too slow in production)
Enter fullscreen mode Exit fullscreen mode

Hook: Detect Uncached Database Calls

For high-traffic endpoints, flag direct DB calls without caching:

# .claude/hooks/check_cache.py
import json, re, sys

data = json.load(sys.stdin)
content = data.get("tool_input", {}).get("content", "") or ""
fp = data.get("tool_input", {}).get("file_path", "")

# Only check route handlers (not repositories)
if not fp or "routes" not in fp and "controllers" not in fp:
    sys.exit(0)

# DB calls without cache check nearby
if re.search(r'prisma\.(user|product|config)\.findMany', content):
    if 'cache.get' not in content and 'cache.set' not in content:
        print("[CACHE] DB call without caching in route handler", file=sys.stderr)

sys.exit(0)
Enter fullscreen mode Exit fullscreen mode

Testing Cached Functions

Generate tests for the cached getUserProfile function.
Test cases:
- Cache hit: returns cached data without DB call
- Cache miss: fetches from DB, stores in cache, returns data
- DB not found: returns null, nothing stored in cache
- Redis error: fetches from DB as fallback (graceful degradation)

Mock both the Redis client and Prisma client.
Enter fullscreen mode Exit fullscreen mode

Code Review Pack (¥980) on PromptWorks.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Security-focused Claude Code engineer.

Top comments (0)