caching-patterns

Caching strategy patterns: Cache-Aside, Write-Through, Write-Behind, TTL design, cache invalidation, Redis patterns, CDN caching, HTTP cache headers, and cache stampede prevention. Caching is both a performance and correctness problem.

8 stars

Best use case

caching-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Caching strategy patterns: Cache-Aside, Write-Through, Write-Behind, TTL design, cache invalidation, Redis patterns, CDN caching, HTTP cache headers, and cache stampede prevention. Caching is both a performance and correctness problem.

Teams using caching-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/caching-patterns/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/caching-patterns/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/caching-patterns/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How caching-patterns Compares

Feature / Agentcaching-patternsStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Caching strategy patterns: Cache-Aside, Write-Through, Write-Behind, TTL design, cache invalidation, Redis patterns, CDN caching, HTTP cache headers, and cache stampede prevention. Caching is both a performance and correctness problem.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Caching Patterns Skill

## When to Activate

- Database queries are slow or overloaded
- External API calls need to be rate-limited or throttled
- High read:write ratio on any data
- Static or semi-static content being served dynamically
- Setting up CDN or HTTP caching headers
- Cache invalidation bugs causing stale data

---

## Pattern Selection

```
Read-heavy, simple data?         → Cache-Aside (Lazy Loading)
Write-heavy, consistency needed? → Write-Through
High write volume, async OK?     → Write-Behind (Write-Back)
Immutable or rarely changes?     → Cache forever, invalidate on change
Complex aggregation?             → Computed cache with explicit invalidation
```

---

## Pattern 1: Cache-Aside (Lazy Loading)

The most common pattern. Application manages the cache explicitly.

```typescript
// Redis Cache-Aside
async function getUser(userId: string): Promise<User> {
  const cacheKey = `user:${userId}`;

  // 1. Check cache first
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // 2. Cache miss — fetch from DB
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  if (!user) throw new NotFoundError(`User ${userId} not found`);

  // 3. Store in cache with TTL
  await redis.setex(cacheKey, 3600, JSON.stringify(user)); // 1 hour TTL

  return user;
}

// Invalidation: call after any write
async function invalidateUser(userId: string) {
  await redis.del(`user:${userId}`);
}
```

**Pros:** Simple, only caches what's actually read, tolerates cold starts
**Cons:** Cache miss on first read (thundering herd risk), data can be stale up to TTL

---

## Pattern 2: Write-Through

Cache and DB updated together on every write. Strong consistency.

```typescript
async function updateUser(userId: string, data: Partial<User>): Promise<User> {
  // 1. Write to DB
  const [updated] = await db
    .update(users)
    .set(data)
    .where(eq(users.id, userId))
    .returning();

  // 2. Update cache immediately (same transaction context)
  const cacheKey = `user:${userId}`;
  await redis.setex(cacheKey, 3600, JSON.stringify(updated));

  return updated;
}
```

**Pros:** Cache always consistent with DB
**Cons:** Every write hits both DB and cache (higher write latency), cache polluted with rarely-read data

---

## Pattern 3: Write-Behind (Write-Back)

Write to cache immediately, persist to DB asynchronously. Highest write throughput.

```typescript
// Write immediately to cache, queue DB write
async function incrementCounter(key: string, amount = 1) {
  // Atomic increment in Redis (no read-modify-write race)
  const newValue = await redis.incrby(`counter:${key}`, amount);

  // Queue DB persistence (non-blocking)
  await queue.add('persist-counter', { key, value: newValue }, {
    delay: 5000,      // Batch writes: persist after 5s of no activity
    jobId: `counter:${key}`,  // Deduplication: only 1 job per key
    removeOnComplete: true,
  });

  return newValue;
}

// Worker: flushes to DB
queue.process('persist-counter', async (job) => {
  const { key, value } = job.data;
  await db.update(counters).set({ value }).where(eq(counters.key, key));
});
```

**Pros:** Extremely fast writes, batches DB load
**Cons:** Data loss risk if cache fails before persistence, complex failure handling

---

## Cache Invalidation Strategies

```typescript
// Strategy 1: TTL-based (simplest, eventual consistency)
await redis.setex(key, ttlSeconds, value);

// Strategy 2: Event-based invalidation (strongest consistency)
// After any mutation, broadcast invalidation event
async function invalidateOnWrite(entity: string, id: string) {
  const patterns = [
    `${entity}:${id}`,           // Single item
    `${entity}:list:*`,          // All list caches for this type
    `user:${userId}:${entity}s`, // Derived caches
  ];
  await Promise.all(patterns.map(p => redis.del(p)));
}

// Strategy 3: Cache versioning (avoids stampede on deploy)
const CACHE_VERSION = process.env.DEPLOY_SHA?.slice(0, 8) ?? 'v1';
function cacheKey(key: string) {
  return `${CACHE_VERSION}:${key}`;
}
// Old version keys expire naturally; no explicit invalidation needed
```

---

## Cache Stampede Prevention

When a popular cache entry expires, thousands of concurrent requests hit the DB simultaneously.

```typescript
// Solution: Probabilistic Early Recomputation (PER)
// Recompute slightly before TTL expires to avoid thundering herd
async function getWithPER<T>(
  key: string,
  ttl: number,
  fetchFn: () => Promise<T>,
  beta = 1.0   // higher = more eager recomputation
): Promise<T> {
  const raw = await redis.get(key);
  if (raw) {
    const { value, expiry } = JSON.parse(raw);
    const remainingTtl = expiry - Date.now() / 1000;
    // Recompute early with probability proportional to staleness
    if (remainingTtl > 0 && -beta * Math.log(Math.random()) < remainingTtl) {
      return value;
    }
  }

  const value = await fetchFn();
  await redis.setex(key, ttl, JSON.stringify({
    value,
    expiry: Date.now() / 1000 + ttl,
  }));
  return value;
}

// Solution 2: Lock (simpler but blocks)
async function getWithLock<T>(
  key: string,
  lockKey: string,
  fetchFn: () => Promise<T>,
  ttl = 300
): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  // Acquire lock — only 1 process fetches
  const acquired = await redis.set(lockKey, '1', 'NX', 'EX', 10);
  if (!acquired) {
    // Another process is fetching — poll briefly
    await new Promise(r => setTimeout(r, 100));
    return getWithLock(key, lockKey, fetchFn, ttl);
  }

  try {
    const value = await fetchFn();
    await redis.setex(key, ttl, JSON.stringify(value));
    return value;
  } finally {
    await redis.del(lockKey);
  }
}
```

---

## HTTP Cache Headers

```typescript
// Express middleware for HTTP caching
function cacheControl(opts: {
  maxAge?: number;      // Browser cache (seconds)
  sMaxAge?: number;     // CDN cache (seconds)
  staleWhileRevalidate?: number;
  noStore?: boolean;
}) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (opts.noStore) {
      res.setHeader('Cache-Control', 'no-store');
    } else {
      const directives = [
        'public',
        opts.maxAge !== undefined && `max-age=${opts.maxAge}`,
        opts.sMaxAge !== undefined && `s-maxage=${opts.sMaxAge}`,
        opts.staleWhileRevalidate !== undefined &&
          `stale-while-revalidate=${opts.staleWhileRevalidate}`,
      ].filter(Boolean);
      res.setHeader('Cache-Control', directives.join(', '));
    }
    next();
  };
}

// Usage
app.get('/api/v1/products', cacheControl({ maxAge: 60, sMaxAge: 3600 }), handler);
app.get('/api/v1/users/me', cacheControl({ noStore: true }), handler);
// Static assets: cache for 1 year (content-addressable filenames)
app.use('/assets', cacheControl({ maxAge: 31536000, sMaxAge: 31536000 }), express.static('dist'));
```

---

## TTL Design Guide

| Data type | Recommended TTL | Reason |
|-----------|----------------|--------|
| User session | 15-30 min (sliding) | Security |
| User profile | 5-60 min | Rarely changes |
| Product catalog | 1-24 hours | Business-controlled updates |
| Search results | 5-15 min | Freshness vs. cost |
| Computed aggregates | 1-5 min | High compute cost |
| Static config | Until deploy | Version-invalidate on deploy |
| Rate limit counters | Match window (60s) | Functional requirement |
| Auth tokens | Token expiry | Must match exactly |

---

## Checklist

- [ ] Cache key includes all dimensions that affect the result (user, locale, version)
- [ ] TTL set on every key — no keys without expiry
- [ ] Cache invalidation implemented alongside every write path
- [ ] Stampede prevention for high-traffic keys (PER or lock)
- [ ] No sensitive PII or secrets stored in shared cache
- [ ] `Cache-Control: no-store` on all authenticated/personal endpoints
- [ ] CDN cache headers set on public static content
- [ ] Cache hit/miss ratio monitored via metrics
- [ ] Graceful degradation: app works if cache is down (circuit breaker)

Related Skills

zero-trust-patterns

8
from marvinrichter/clarc

Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.

webrtc-patterns

8
from marvinrichter/clarc

WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.

webhook-patterns

8
from marvinrichter/clarc

Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.

wasm-patterns

8
from marvinrichter/clarc

WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).

ux-micro-patterns

8
from marvinrichter/clarc

UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.

typescript-patterns

8
from marvinrichter/clarc

TypeScript patterns — type system best practices, strict mode, utility types, generics, discriminated unions, error handling with Result types, and module organization. Core patterns for production TypeScript.

typescript-patterns-advanced

8
from marvinrichter/clarc

Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.

typescript-monorepo-patterns

8
from marvinrichter/clarc

TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.

terraform-patterns

8
from marvinrichter/clarc

Infrastructure as Code with Terraform — project structure, remote state, modules, workspace strategy, AWS/GCP patterns, CI/CD integration, and security hardening. The standard for managing production infrastructure.

swiftui-patterns

8
from marvinrichter/clarc

SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.

swift-patterns

8
from marvinrichter/clarc

Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development.

swift-patterns-advanced

8
from marvinrichter/clarc

Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.