Caching Strategies
Purpose
Databases are slow; caches are fast. This skill teaches teams to strategically cache frequently accessed data in fast storage (Redis, Memcached) to intercept requests before they hit the database, dramatically reducing latency and database load. Caching is NOT a substitute for proper database optimization.
When to use
- System is experiencing high latency on read-heavy API endpoints
- Database CPU/Memory usage is constantly peaking due to repeated queries
- Designing a new system expecting massive read throughput
- Content rarely changes but reads happen constantly
When NOT to use
- Before optimizing database queries (optimize first, cache second)
- For data that changes frequently or is highly personalized
- When consistency is more important than performance
- As a quick fix for poor application architecture
Inputs required
- Slow query logs or endpoint metrics
- Cache infrastructure (Redis, Memcached)
- Read/write patterns of the data
- Acceptable staleness window (TTL)
Workflow
- Identify Bottlenecks: Profile APIs to find read-heavy, slow-changing data (user profiles, static config, product catalogs)
- Measure Access Patterns: How often is data read vs. written? How large? How old can it be?
- Select Strategy:
- Cache-Aside: App checks cache; on miss, fetches DB, writes cache
- Write-Through: App writes to cache and DB simultaneously
- Write-Behind: App writes to cache only; batch updates to DB later (use cautiously)
- Set Eviction Policy: Assign Time-To-Live (TTL) values based on business tolerance for stale data
- Handle Invalidation: Implement cache invalidation logic on mutations (UPDATE/DELETE)
- Prevent Stampedes: Implement caching locks or staggered TTLs to prevent multiple clients fetching from DB simultaneously when a key expires
- Add Monitoring: Track cache hit rates, eviction rates, and staleness
Rules
- MUST treat cache as ephemeral (can disappear at any time)
- MUST NOT cache highly personalized or sensitive data (PII) in shared caches
- MUST ALWAYS implement fallback to primary data store on cache failure
- MUST set TTL values (no infinite cache)
- MUST invalidate cache on mutations (UPDATE, DELETE operations)
- MUST use separate caches for different data types/sensitivity levels
- MUST NOT use cache as source of truth
Anti-patterns
- Infinite TTL: Storing keys without expiration (leads to unbounded memory, stale data)
- Premature Caching: Adding Redis before the database is properly indexed
- No Fallback: Cache failure crashes the application
- Over-Caching: Caching everything including rarely-used data (wastes cache memory)
- Forgetting Invalidation: Updating database but not cache (stale data served indefinitely)
- Cache Stampede: All requests hit cache miss simultaneously, causing DB spike
- Storing Mutable Objects: Storing references to objects that change, returning stale mutations
Failure conditions
- Cache becomes source of truth (data lost if cache clears)
- No fallback logic when cache is unavailable
- Cache miss causes cascading database failures
- Stale data leads to data corruption
- Cache stampede (all requests simultaneously fetch from DB)
Validation checklist
Output format
- Cache-Aside wrapper: Function that checks cache, handles miss, populates cache
- Invalidation triggers: ON UPDATE/DELETE, clear cache key
- Configuration: TTL values, eviction policy, cache key naming convention
- Monitoring: Hit rates, eviction rates, staleness metrics
- Documentation: Which data is cached, staleness tolerance, fallback behavior
Security considerations
- Sensitive data MUST NOT be cached or only in encrypted form
- Cache keys MUST be predictable (not guessable)
- Access control MUST be enforced (not all users can access all cache keys)
- Sensitive data MUST be masked in monitoring logs
- Cache MUST be protected from unauthorized access
Agent execution notes
- Agent MAY: Add cache checks, implement invalidation, set TTL, add monitoring
- Agent MUST NEVER: Use cache as source of truth, forget fallback logic, set infinite TTL
- Agent MUST ASK: Before caching sensitive data, before major caching strategy change
- Agent MUST VALIDATE: Fallback logic works, TTL configured, invalidation on mutations
Example
❌ Anti-pattern (No fallback, infinite TTL, no invalidation):
// WRONG: No fallback if cache fails
const getUser = async (userId) => {
return redis.get(`user:${userId}`); // Fails if Redis is down
};
// WRONG: Infinite TTL
redis.set(`config:app-settings`, settings); // Never expires = stale data forever
// WRONG: No invalidation on update
async function updateUser(userId, data) {
await db.users.update(userId, data);
// Forgot to clear cache - stale data served
}
// WRONG: Cache stampede - all requests hit DB when key expires
const getExpensiveData = async () => {
const cached = await redis.get('expensive-data');
if (!cached) {
// All concurrent requests hit this - DB spike
const data = await slowQuery();
await redis.set('expensive-data', data, { ex: 3600 });
return data;
}
return cached;
};
✅ Correct pattern (Fallback, TTL, invalidation, stampede prevention):
// CORRECT: Fallback to database on cache miss or failure
const getUser = async (userId) => {
try {
const cached = await redis.get(`user:${userId}`);
if (cached) return JSON.parse(cached);
} catch (e) {
logger.warn('Cache miss, falling back to DB', e);
}
// Fallback to DB
const user = await db.users.findById(userId);
// Repopulate cache
try {
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user)); // 1 hour TTL
} catch (e) {
logger.warn('Failed to cache, but returning DB data', e);
}
return user;
};
// CORRECT: TTL on all cache entries
redis.setex('config:app-settings', 86400, JSON.stringify(settings)); // 24 hour TTL
// CORRECT: Invalidate cache on mutations
async function updateUser(userId, data) {
await db.users.update(userId, data);
await redis.del(`user:${userId}`); // Clear specific key
}
// CORRECT: Prevent cache stampede with locks
const stampedeLock = new Mutex();
const getExpensiveData = async () => {
const cached = await redis.get('expensive-data');
if (cached) return JSON.parse(cached);
// Use lock to ensure only one request fetches from DB
return stampedeLock.runExclusive(async () => {
// Double-check if another request already populated cache
const cached = await redis.get('expensive-data');
if (cached) return JSON.parse(cached);
const data = await slowQuery();
await redis.setex('expensive-data', 3600, JSON.stringify(data));
return data;
});
};
1---2name: caching-strategies3description: When improving read performance and reducing database load.4license: MIT5---67# Caching Strategies89## Purpose10Databases are slow; caches are fast. This skill teaches teams to strategically cache frequently accessed data in fast storage (Redis, Memcached) to intercept requests before they hit the database, dramatically reducing latency and database load. Caching is NOT a substitute for proper database optimization.1112## When to use13- System is experiencing high latency on read-heavy API endpoints14- Database CPU/Memory usage is constantly peaking due to repeated queries15- Designing a new system expecting massive read throughput16- Content rarely changes but reads happen constantly1718## When NOT to use19- Before optimizing database queries (optimize first, cache second)20- For data that changes frequently or is highly personalized21- When consistency is more important than performance22- As a quick fix for poor application architecture2324## Inputs required25- Slow query logs or endpoint metrics26- Cache infrastructure (Redis, Memcached)27- Read/write patterns of the data28- Acceptable staleness window (TTL)2930## Workflow311. **Identify Bottlenecks**: Profile APIs to find read-heavy, slow-changing data (user profiles, static config, product catalogs)322. **Measure Access Patterns**: How often is data read vs. written? How large? How old can it be?333. **Select Strategy**:34 - *Cache-Aside*: App checks cache; on miss, fetches DB, writes cache35 - *Write-Through*: App writes to cache and DB simultaneously36 - *Write-Behind*: App writes to cache only; batch updates to DB later (use cautiously)374. **Set Eviction Policy**: Assign Time-To-Live (TTL) values based on business tolerance for stale data385. **Handle Invalidation**: Implement cache invalidation logic on mutations (UPDATE/DELETE)396. **Prevent Stampedes**: Implement caching locks or staggered TTLs to prevent multiple clients fetching from DB simultaneously when a key expires407. **Add Monitoring**: Track cache hit rates, eviction rates, and staleness4142## Rules43- MUST treat cache as ephemeral (can disappear at any time)44- MUST NOT cache highly personalized or sensitive data (PII) in shared caches45- MUST ALWAYS implement fallback to primary data store on cache failure46- MUST set TTL values (no infinite cache)47- MUST invalidate cache on mutations (UPDATE, DELETE operations)48- MUST use separate caches for different data types/sensitivity levels49- MUST NOT use cache as source of truth5051## Anti-patterns52- **Infinite TTL**: Storing keys without expiration (leads to unbounded memory, stale data)53- **Premature Caching**: Adding Redis before the database is properly indexed54- **No Fallback**: Cache failure crashes the application55- **Over-Caching**: Caching everything including rarely-used data (wastes cache memory)56- **Forgetting Invalidation**: Updating database but not cache (stale data served indefinitely)57- **Cache Stampede**: All requests hit cache miss simultaneously, causing DB spike58- **Storing Mutable Objects**: Storing references to objects that change, returning stale mutations5960## Failure conditions61- Cache becomes source of truth (data lost if cache clears)62- No fallback logic when cache is unavailable63- Cache miss causes cascading database failures64- Stale data leads to data corruption65- Cache stampede (all requests simultaneously fetch from DB)6667## Validation checklist68- [ ] Cache is treated as optimization layer, not data store69- [ ] Fallback to primary data store on cache miss works70- [ ] TTL is set (no infinite cache)71- [ ] Cache invalidation triggered on data mutations72- [ ] Cache hit rate > 80% for target queries73- [ ] No cache stampede when keys expire simultaneously74- [ ] Sensitive data not cached (or encrypted if cached)75- [ ] Cache memory usage monitored (no unbounded growth)76- [ ] Stale data handling defined (acceptable staleness window)77- [ ] Monitoring dashboards show hit rate and eviction rate7879## Output format80- **Cache-Aside wrapper**: Function that checks cache, handles miss, populates cache81- **Invalidation triggers**: ON UPDATE/DELETE, clear cache key82- **Configuration**: TTL values, eviction policy, cache key naming convention83- **Monitoring**: Hit rates, eviction rates, staleness metrics84- **Documentation**: Which data is cached, staleness tolerance, fallback behavior8586## Security considerations87- Sensitive data MUST NOT be cached or only in encrypted form88- Cache keys MUST be predictable (not guessable)89- Access control MUST be enforced (not all users can access all cache keys)90- Sensitive data MUST be masked in monitoring logs91- Cache MUST be protected from unauthorized access9293## Agent execution notes94- Agent MAY: Add cache checks, implement invalidation, set TTL, add monitoring95- Agent MUST NEVER: Use cache as source of truth, forget fallback logic, set infinite TTL96- Agent MUST ASK: Before caching sensitive data, before major caching strategy change97- Agent MUST VALIDATE: Fallback logic works, TTL configured, invalidation on mutations9899## Example100101**❌ Anti-pattern (No fallback, infinite TTL, no invalidation):**102```javascript103// WRONG: No fallback if cache fails104const getUser = async (userId) => {105 return redis.get(`user:${userId}`); // Fails if Redis is down106};107108// WRONG: Infinite TTL109redis.set(`config:app-settings`, settings); // Never expires = stale data forever110111// WRONG: No invalidation on update112async function updateUser(userId, data) {113 await db.users.update(userId, data);114 // Forgot to clear cache - stale data served115}116117// WRONG: Cache stampede - all requests hit DB when key expires118const getExpensiveData = async () => {119 const cached = await redis.get('expensive-data');120 if (!cached) {121 // All concurrent requests hit this - DB spike122 const data = await slowQuery();123 await redis.set('expensive-data', data, { ex: 3600 });124 return data;125 }126 return cached;127};128```129130**✅ Correct pattern (Fallback, TTL, invalidation, stampede prevention):**131```javascript132// CORRECT: Fallback to database on cache miss or failure133const getUser = async (userId) => {134 try {135 const cached = await redis.get(`user:${userId}`);136 if (cached) return JSON.parse(cached);137 } catch (e) {138 logger.warn('Cache miss, falling back to DB', e);139 }140 141 // Fallback to DB142 const user = await db.users.findById(userId);143 144 // Repopulate cache145 try {146 await redis.setex(`user:${userId}`, 3600, JSON.stringify(user)); // 1 hour TTL147 } catch (e) {148 logger.warn('Failed to cache, but returning DB data', e);149 }150 151 return user;152};153154// CORRECT: TTL on all cache entries155redis.setex('config:app-settings', 86400, JSON.stringify(settings)); // 24 hour TTL156157// CORRECT: Invalidate cache on mutations158async function updateUser(userId, data) {159 await db.users.update(userId, data);160 await redis.del(`user:${userId}`); // Clear specific key161}162163// CORRECT: Prevent cache stampede with locks164const stampedeLock = new Mutex();165const getExpensiveData = async () => {166 const cached = await redis.get('expensive-data');167 if (cached) return JSON.parse(cached);168 169 // Use lock to ensure only one request fetches from DB170 return stampedeLock.runExclusive(async () => {171 // Double-check if another request already populated cache172 const cached = await redis.get('expensive-data');173 if (cached) return JSON.parse(cached);174 175 const data = await slowQuery();176 await redis.setex('expensive-data', 3600, JSON.stringify(data));177 return data;178 });179};180```