🗄️ Database Indexing & Caching
Cache-Aside, Write-Through & Write-Behind
Cache-Aside, Write-Through, Write-Behind, and Read-Through — when to use each
Caching is about moving reads and writes away from disk-bound databases toward memory-speed storage. But each pattern makes different tradeoffs between consistency, availability, and latency. Choosing the wrong pattern causes either stale reads or lost writes.
Key Concepts
Cache-Aside (Lazy Loading)
App checks cache first. On miss, app reads DB and populates cache. Simple, resilient — cache can crash without failing the application. Standard choice for 90% of use cases.
Write-Through
Every write goes to cache AND DB synchronously. Cache is always fresh. Tradeoff: higher write latency (two writes). Good when read-freshness is critical (user profile, config).
Write-Behind (Write-Back)
Write to cache only. Background worker flushes to DB asynchronously in batches. Extreme write throughput. Risk: cache crash = data loss. Use only for non-critical data (analytics, metrics).
Read-Through
Cache sits in front of DB. On miss, the cache itself fetches from DB (not your app code). Keeps cache warm automatically. Used in caching libraries like NCache or Ehcache.
Cache-Aside Architecture
Write-Through Architecture
Write-Behind Architecture
Cache-Aside Implementation (NestJS + Redis)
product.service.tstypescript
1@Injectable()
2export class ProductService {
3 private readonly CACHE_TTL = 300; // 5 minutes
4
5 constructor(
6 private redis: Redis,
7 private productRepo: ProductRepository,
8 ) {}
9
10 async getProduct(id: string): Promise<Product> {
11 const cacheKey = `product:${id}`;
12
13 // 1. Check cache first
14 const cached = await this.redis.get(cacheKey);
15 if (cached) {
16 return JSON.parse(cached); // Cache HIT — ~1ms
17 }
18
19 // 2. Cache MISS — fetch from DB (~100ms)
20 const product = await this.productRepo.findById(id);
21 if (!product) throw new NotFoundException();
22
23 // 3. Store in cache with TTL
24 await this.redis.setex(cacheKey, this.CACHE_TTL, JSON.stringify(product));
25
26 return product;
27 }
28
29 async updateProduct(id: string, dto: UpdateProductDto): Promise<Product> {
30 const product = await this.productRepo.update(id, dto);
31
32 // Invalidate cache on write — Cache-Aside consistency
33 await this.redis.del(`product:${id}`);
34
35 return product;
36 }
37}Pattern Selection Guide
| Pattern | Write Latency | Data Freshness | Crash Risk | Best Use Case |
|---|---|---|---|---|
| Cache-Aside | DB only | Eventual (TTL) | None | Product pages, user profiles |
| Write-Through | Cache + DB | Always fresh | None | Config, session data, settings |
| Write-Behind | Cache only (fast!) | Delayed | Data loss possible | Analytics, click tracking, views count |
| Read-Through | DB only | Eventual (TTL) | None | Library-managed caching scenarios |
💡
Senior Architect Insight: TTL (Time-To-Live) is not cache invalidation — it's cache expiry. True invalidation requires you to explicitly delete/update the cache key when the underlying data changes. Most cache consistency bugs come from relying on TTL alone for mutable data.