| Layer | Caches | Invalidated by | BoxOffice |
|---|---|---|---|
| Client (browser, app) | HTTP responses (Cache-Control, ETag) |
headers only — cannot recall | versioned JS, event JSON max-age=10 |
| CDN | static assets, public pages | purge API, versioned URLs | posters, public event page |
| API gateway | whole GET responses |
short TTL | GET /events/42 for 5 s |
| In-process (Caffeine) | hot objects in the heap | each replica separately | venue geometry |
| Distributed (Redis, Valkey) | objects, query results, sessions | app (delete) + TTL | event details, search |
| Database | pages in the buffer pool | the engine — always consistent | seats index pages |
Closer to the user: faster and cheaper per hit, but harder to invalidate.
| Hit ratio | Effective latency (0.5 / 8.5 ms) | DB load at 50,000 reads/s |
|---|---|---|
| 90% | 1.3 ms | 5,000 QPS |
| 99% | 0.58 ms | 500 QPS |
| 99.9% | 0.51 ms | 50 QPS |
Think in miss ratio: each extra nine is 10× less database load.
MEMORY USAGE); watch evictions vs hit ratio; keep headroom.Cache what you display, not what you decide on. The seat map may be 1 s stale; the seat hold is decided in a SQL transaction. Same for prices at checkout, payments, balances.
Read: GET → hit? return. Miss → read DB → SET key EX ttl → return.
Write: update DB → commit → DEL key.
@Cacheable(cacheNames = "events", key = "#id")
public EventDto findById(long id) { ... }
@CacheEvict(cacheNames = "events", key = "#id") // after commit!
public EventDto update(long id, EventUpdate change) { ... }
| Pattern | Idea | Good | Risk | BoxOffice |
|---|---|---|---|---|
| Read-through | the cache loads on miss | coalesces concurrent misses | — | venue geometry (Caffeine) |
| Write-through | write cache + DB together | fresh reads after writes | two writes, not atomic | organizer dashboard |
| Write-behind | write cache now, DB later | memory-speed writes | loses acked writes | view counters |
| Write-around | write DB, skip cache | no pollution | first read misses | tickets, order history |
| Refresh-ahead | reload before expiry | hot keys never miss | wasted refreshes | today's on-sales |
Write-behind for seat holds = double booking after a cache failure.
boxoffice:catalog:event:42:v3.| Data | Staleness OK | Mechanism |
|---|---|---|
| Event description | ~1 min | delete on write + TTL 5 min |
| Price table | seconds | TTL 30 s + delete; checkout reads the DB |
| Seat map (display) | 1–2 s | TTL 1 s |
| User profile | read-your-writes for the owner | delete + bypass for the writer |
| Seat hold, order, payment | zero | not cached |
Decide the budget per data type with the product team; the mechanism follows.
v1 → writer commits v2 + DEL → reader SET v1.DEL + pub/sub to L1.| Policy | Keeps | Weakness |
|---|---|---|
| LRU | recently used | one scan flushes the hot set |
| LFU | frequently used | slow to forget (needs aging) |
| FIFO / random | — | ignore popularity |
| W-TinyLFU (Caffeine) | frequency-admitted + small LRU window | more complex |
allkeys-lru / allkeys-lfu for caches; noeviction for sessions and locks.maxmemory-samples 5).| Failure | Symptom | Fix |
|---|---|---|
| Cold cache | DB gets everything after restart/deploy | warm-up, gradual shift |
| Stampede | hot key expires, N identical queries | coalescing, lock, SWR, XFetch |
| Avalanche | many keys expire together | TTL jitter |
| Penetration | ids that do not exist | negative cache, Bloom filter |
| Hot key | one node saturates | L1 cache, key replication |
| Cache down | DB sized for misses gets 100% | timeouts, breaker, load shedding |
event:42: 20,000 reads/s; recompute takes 200 ms. The key expires. How many identical queries hit the DB?
SET lock:key token NX PX 5000; losers wait and re-read.Recompute if
No locks, no coordination: the algorithm adapts to the traffic.
Hot key: 400,000 reads/s × 4 KB ≈ 13 Gbit/s on one node.
event:42#0..7, CDN for the static part.Cache down: the DB was sized for the misses.
Once the DB depends on the hit ratio, the cache is a load-bearing dependency.
MULTI/EXEC (no rollback), Lua scripts / functions.everysec) · both.slot = CRC16(key) mod 16384; each primary owns a range.MOVED / ASK redirect when it changes.{event:42}:seats, {event:42}:prices.{boxoffice}:* puts the whole app on one node — tag by something with many values.
> SET hold:42:A-12 fan:1017 NX PX 600000
OK
> SET hold:42:A-12 fan:2203 NX PX 600000
(nil)
If Redis loses a pre-hold, two fans reach the DB and one loses there. Correctness never depended on Redis.
| Component | Question it answers | Key concepts |
|---|---|---|
client mobile |
can the device skip the request? | Cache-Control, ETag, versioned assets |
cdn apigw |
can the edge answer for the origin? | public pages, short TTLs, purge by tag |
app |
can each replica keep the hottest objects? | in-process L1, two-level cache |
cache |
can a shared copy absorb most reads? | cache-aside, TTL, eviction, hit ratio, sizing |
cache (under stress) |
hot key expires? cache dies? | stampede, avalanche, hot keys, fallback |
sql |
what does the cache protect? | source of truth — cache what you display, not what you decide |
stream pubsub |
how does a change reach every copy? | CDC from the WAL, broadcast to L1 |
Each layer is faster and closer to the user but holds less and is harder to invalidate.
Outline: (1) why caches work and where they live; (2) hit-ratio math and sizing; (3) patterns; (4) invalidation and eviction; (5) failure modes; (6) Redis.
Let the class answer first. Lead them to: the work is redundant, so remove it.
Draw the race on the board as a timeline with four steps.