APIs without caching run the same DB queries repeatedly. Redis can make responses 10x faster — but wrong cache design causes data inconsistency. Claude Code generates the safe caching patterns from CLAUDE.md rules.
CLAUDE.md for Cache Rules
## Redis Cache Design Rules
### Patterns
- Cache-Aside: high-read/low-write data (user profiles, product catalog)
- Write-Through: data requiring strong consistency (balances, inventory)
- Pub/Sub: cache invalidation propagation (distributed environments)
### TTL (required)
- All caches must have TTL (no permanent caches)
- Master data: TTL 1 hour
- Sessions/profiles: TTL 15 minutes
- Aggregations: TTL 5 minutes
### Cache Keys
- Format: {service}:{entity}:{id}
- Examples: user:profile:123, product:detail:abc
- Versioning: user:v2:profile:123 (bump version on schema changes)
### Monitoring
- Track cache hit ratio as a metric
- Design for cold start (unwarmed cache) scenarios
Generating Cache-Aside Pattern
Generate Cache-Aside cache for user profiles.
Requirements:
- Cache key: user:profile:{userId}
- TTL: 15 minutes
- On cache miss: fetch from DB and populate cache
- On profile update: invalidate cache
- Log cache hit/miss
Save to: src/services/userCacheService.ts
Generated:
// src/services/userCacheService.ts
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
const CACHE_TTL = 15 * 60;
const KEY_PREFIX = 'user:profile';
function cacheKey(userId: string): string {
return `${KEY_PREFIX}:${userId}`;
}
export async function getUserProfile(userId: string): Promise<UserProfile | null> {
const key = cacheKey(userId);
const cached = await redis.get(key);
if (cached) {
logger.debug({ userId, cacheHit: true }, 'User profile cache hit');
return JSON.parse(cached) as UserProfile;
}
logger.debug({ userId, cacheHit: false }, 'User profile cache miss');
const user = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, name: true, email: true, avatarUrl: true, updatedAt: true },
});
if (!user) return null;
await redis.set(key, JSON.stringify(user), { EX: CACHE_TTL });
return user;
}
export async function invalidateUserProfile(userId: string): Promise<void> {
await redis.del(cacheKey(userId));
logger.info({ userId }, 'User profile cache invalidated');
}
// Invalidate on update
export async function updateUserProfile(userId: string, data: UpdateProfileInput) {
const updated = await prisma.user.update({ where: { id: userId }, data });
await invalidateUserProfile(userId);
return updated;
}
Write-Through Pattern
// src/services/inventoryCacheService.ts
// Inventory: consistency-critical → Write-Through
export async function updateInventory(productId: string, quantity: number): Promise<void> {
await prisma.$transaction(async (tx) => {
await tx.inventory.update({ where: { productId }, data: { quantity } });
});
// After DB success, update cache too (Write-Through)
await redis.set(
`inventory:${productId}`,
JSON.stringify({ quantity, updatedAt: new Date() }),
{ EX: 5 * 60 }
);
}
Distributed Cache Invalidation (Pub/Sub)
// Sync cache invalidation across multiple servers
async function publishCacheInvalidation(channel: string, key: string): Promise<void> {
await publisher.publish(channel, JSON.stringify({ key, timestamp: Date.now() }));
}
async function subscribeCacheInvalidation(): Promise<void> {
await subscriber.subscribe('cache:invalidate', async (message) => {
const { key } = JSON.parse(message);
await redis.del(key);
logger.info({ key }, 'Cache invalidated via pub/sub');
});
}
Summary
Design Redis caching with Claude Code:
- CLAUDE.md — TTL required on all caches, key format standard, pattern selection criteria
- Cache-Aside — Reduce read load (invalidate on write)
- Write-Through — Update cache atomically with DB writes
- Pub/Sub — Propagate cache invalidation across distributed servers
Code Review Pack (¥980) includes /code-review for cache review — missing TTL, cache stampede risks, consistency issues.
Myouga (@myougatheaxo) — Claude Code engineer focused on performance and caching.
Top comments (0)