Module 8 · Performance and caching

Lesson 38 — Caching strategies

What to cache, how to invalidate and what to do about stale data.

Published
In this lesson
  1. Exercise 1 — The filter
  2. Exercise 2 — Cache-aside with single-flight
  3. Exercise 3 — HTTP caching
  4. Exercise 4 — Invalidation by events
  5. Exercise 5 — The final measurement
  6. Submit

The cache that's actually worth it, measured. No solutions.md before submitting.

Exercise 1 — The filter

  1. Apply the two questions to 8 TicketFlow data items: published listing, event detail, seat availability, org tariffs, feature config (27), user profile, cart/active reservation, search results. Table: data | can be stale | invalidatable | decision.
  2. The forbidden-cache finding: search your code for cache.get/set over data failing the filter (cached availability deciding a purchase? permissions cached without a TTL?). Document each violation with the concrete risk.
  3. The FLUSHALL test: in staging, wipe ALL the Redis and verify the system degrades slow but correct (the cache is rebuildable). If something breaks: it is a write-behind in disguise — find it.

Exercise 2 — Cache-aside with single-flight

  1. Implement listado_protected (§4's version with the NX lock + stale) for the per-city listing with :v3 and TTL with jitter.
  2. The stampede test: 50 threads over the cold key with CaptureQueriesContext — expected ≤3 queries. Run the test WITHOUT protection first (50 queries): paste the disaster's evidence.
  3. The shape version: change the listing's dict (add currency), bump to :v4 and verify the old v3 keys don't break anything (they expire or get ignored). How long does the key migration take in real traffic? (answer: zero invalidation deploys).

Exercise 3 — HTTP caching

  1. Add Cache-Control: public, max-age=60 + ETag to the public listing and detail. Test: 2 identical GETs → the second is a 304 with an empty body (the test client must send If-None-Match).
  2. The per-endpoint decision: which endpoints NEVER carry public Cache-Control (reservations, profile, payments) and which header they carry instead (private, no-store)? A test verifying it on all 3.
  3. The imagined CDN: write the 8-line ADR (48) "CDN on TicketFlow's listing": what it gains (estimated hit rate with your 36 metric), which invalidation it demands, and why the ETag with max_updated_at makes it safe without purges.

Exercise 4 — Invalidation by events

  1. Wire the stream's consumers (30): EventUpdated → invalidates events:{ciudad}:vN and the detail; EventPublished → invalidates the city's listing. Test: publish an event → cache.set with old data → consume the event → cache.get returns the new one.
  2. The safety net: break the invalidation on purpose (the consumer fails). The TTL saves the data: in how many seconds at most does the system converge? Write the guarantee in one line (32: defined convergence).
  3. PII in the cache (23): the user profile cached with a 60 s TTL and per-user purge (cache.delete_pattern) in the GDPR export. Test: request the export → the cached profile gets purged → the next GET rebuilds without the deleted data.

Exercise 5 — The final measurement

  1. Before/after with k6 (36): the browse plateau WITHOUT cache vs WITH cache — p95, hits/misses, and the DB (pg_stat_statements): how much did the listing queries' total_time drop?
  2. The cache's cost: how many MB of Redis do your keys take? What is the eviction policy (maxmemory-policy allkeys-lru?) and what happens to the lock/stale if Redis evicts the lock: key? Document the decision.
  3. The non-cacheable list with its reason (one line per datum) in docs/caching.md: the document preventing next sprint's "cache everything".

Submit

Paste the filter's table, the stampede's evidence without/with protection, the 304 test and the k6 before/after. Next: Lesson 39 — Pools, pagination and compression.