Database Caching at Scale · Capstone Case Studies

Capstone: Caching a Flash-Sale Product Page

500,000 shoppers open the same product page at 10:00:00. Design the caching so the database survives and prices stay right.

Scenario

Example company: a retailer launches a limited console bundle at 10:00:00. Marketing expects 500,000 concurrent visitors, nearly all on one product page and the stock badge ("only 312 left"). The page shows product details (rarely change), price (may be changed by ops during the sale), stock (changes with every purchase) and personalized bits (cart count, recommendations).

Stack: CDN, 60 app pods, Redis Cluster (6 shards), Postgres primary + 2 replicas. Checkout uses the reservation flow from the transactions course.

Requirements

  • The database must never see more than ~2,000 queries/s from this page.
  • A price change by ops must reach all shoppers within 10 seconds.
  • The stock badge may lag by a few seconds, but must never show "in stock" for more than 10 s after selling out.
  • No shopper ever sees another shopper's cart or data.

Deliverables

  1. Split the page into cacheable parts and pick a strategy (layer, TTL, invalidation) for each.
  2. Protect against stampedes and hot keys at 10:00:00.
  3. Keep price and stock fresh enough — and correct at checkout.
  4. Prevent personalized data leaks.
  5. What happens if Redis loses a shard during the sale?

Hints

  • One key, 500,000 readers: think L1 + CDN, not just Redis.
  • Stock is a counter that changes on every purchase — how often does the badge really need to update?
  • What's cached at the edge must be identical for everyone.

Draft each deliverable below before revealing the model answer and rubric.

Further reading: Scaling Memcache at Facebook · RFC 5861: stale-while-revalidate

Deliverables

Split the page and choose a strategy per part.

Model answer

Part Where Freshness
Product details/images CDN (public, max-age=300, stale-while-revalidate) + Redis minutes; purge/generation bump on edit
Price Redis + L1 (2–5 s), event-driven invalidation ≤ 10 s
Stock badge Separate tiny JSON endpoint, CDN max-age=2 + Redis value refreshed by a background job every 1–2 s ≤ a few seconds
Cart count, recommendations Client-side fetch, private, no-store per user

The HTML shell is identical for everyone and edge-cacheable; personalization is loaded separately.

Rubric

Look for: separating static, volatile and personalized parts; edge caching only shared content; explicit freshness per part. Misses: one TTL for the whole page, caching personalized HTML at the CDN.

Stampede and hot-key protection.

Model answer

  • Pre-warm all keys before 10:00 and use refresh-ahead (background job) for the product, price and stock keys — they never expire under load.
  • CDN absorbs most requests; L1 per pod (2 s) for the product and price so Redis sees ~60 pods × 1 req/2 s per key, not 500k/s.
  • Key replication for the stock key across shards if needed (stock:{sku}#0..7).
  • Request coalescing (singleflight + short Redis lock) as a fallback when something does expire.

Rubric

Look for: no expiry of hot keys during the event (refresh-ahead), CDN + L1 to divide load, coalescing as a fallback, awareness of the single-shard limit. Misses: relying on TTL expiry and Redis alone.

Freshness of price and stock, and correctness at checkout.

Model answer

  • Price changes commit to Postgres → outbox/CDC → invalidation consumer deletes Redis keys and publishes an L1 invalidation; L1 TTL 2–5 s as backstop → within 10 s everywhere.
  • Stock badge = a value computed from the database every 1–2 s by one job (not on reads), with "sold out" pushed immediately via invalidation when stock hits zero.
  • Checkout never uses cached price or stock: it reserves with a conditional update and reads the authoritative price in the same transaction; show "price updated" if it differs from what the page showed.

Rubric

Look for: an invalidation pipeline with bounded L1 staleness, stock computed by a single writer, and authoritative reads at checkout. Misses: decrementing a cached stock counter as the source of truth.

Prevent personalized data leaks.

Model answer

  • Personalized endpoints return Cache-Control: private, no-store; the CDN is configured to respect origin headers and bypass on Authorization/session cookies.
  • Shared cached responses contain no user data by construction (shell + public JSON only).
  • Cache keys for any user-dependent server-side data include user/tenant; tests assert two users get different responses.

Rubric

Look for: header discipline, CDN configuration, and tests. Misses: 'we just won't cache the cart' without enforcing it at the CDN.

Redis loses a shard during the sale.

Model answer

  • Pods keep serving from L1 and the CDN keeps serving its copies (stale-while-revalidate) for the hot keys.
  • Cache-miss fallback to the database is bounded (bulkhead, e.g. 50 concurrent per pod cluster-wide budget ~2,000 qps); beyond it, serve the last known value or a degraded page.
  • After failover, bump generations for price keys on that shard and replay recent invalidations, since async replication may have lost deletes.

Rubric

Look for: layered fallbacks (L1/CDN), bounded database access, and post-failover invalidation. Misses: unbounded fallback to Postgres.

Open in the app to mark lessons complete, take notes and track your progress.