MODULE 2/LESSON 4
🗄️ Database Indexing & Caching

Cache-Aside, Write-Through & Write-Behind

Cache-Aside, Write-Through, Write-Behind, and Read-Through — when to use each

16 min📊 Diagram
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

Cache-Aside Pattern (Lazy Loading) Application (API) Redis (Cache) Memory-Speed (~1ms) Postgres (DB) Disk-Speed (~100ms) 1. Check Cache 2. Cache MISS 3. Fetch from DB 4. Return Data 5. Populate Cache (TTL)

Write-Through Architecture

Write-Through Pattern Application (API) Redis (Cache) Postgres (DB) 1. Write to Cache 1. Write to DB Returns success only when both writes complete

Write-Behind Architecture

Write-Behind (Write-Back) Pattern Application Cache Primary Data Store! Database 1. Fast Write 2. Async Batch Flush App doesn't wait for DB. Risk: if Cache dies before flush, data is lost.

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

PatternWrite LatencyData FreshnessCrash RiskBest Use Case
Cache-AsideDB onlyEventual (TTL)NoneProduct pages, user profiles
Write-ThroughCache + DBAlways freshNoneConfig, session data, settings
Write-BehindCache only (fast!)DelayedData loss possibleAnalytics, click tracking, views count
Read-ThroughDB onlyEventual (TTL)NoneLibrary-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.