The cache that's actually worth it, measured. No solutions.md before submitting.
Exercise 1 — The filter
- 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.
- The forbidden-cache finding: search your code for
cache.get/setover data failing the filter (cached availability deciding a purchase? permissions cached without a TTL?). Document each violation with the concrete risk. - 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
- Implement
listado_protected(§4's version with the NX lock + stale) for the per-city listing with:v3and TTL with jitter. - 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. - The shape version: change the listing's dict (add
currency), bump to:v4and 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
- 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 sendIf-None-Match). - 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. - 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
- Wire the stream's consumers (30):
EventUpdated→ invalidatesevents:{ciudad}:vNand 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. - 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).
- 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
- 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?
- 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 thelock:key? Document the decision. - 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.