Il caching è la leva architetturale con la massima resa e la massima tassa. La resa è enorme: un cache layer ben piazzato può assorbire dal 90 al 99 percento del traffico in lettura che altrimenti colpirebbe il database, e il costo dell’hardware della cache è una frazione del costo del database che protegge. La tassa è la complessità che introduce: ogni valore in cache è una copia che può non concordare con la sorgente, ogni TTL è un’ipotesi su quanta staleness sia accettabile, ogni invalidazione è un problema di coordinazione, e il giorno in cui scade una hot key è il giorno in cui il database crolla.
Questa lezione tratta il caching come problema di system design. I tre tier in cui vivono le cache, i quattro pattern canonici per tenere la cache allineata con la sorgente di verità, le strategie di invalidazione che si collocano sull’asse consistency-versus-correctness, e il problema della cache stampede che produce il disservizio cache-related più comune. Gli esempi specifici sono Redis, CloudFront e Memcached perché nel 2026 sono gli strumenti dominanti, ma i pattern sono indipendenti dallo strumento.
I tre tier di caching
Un sistema ragionevolmente architettato ha cache su tre layer logici, ciascuno che assorbe una classe di traffico diversa.
Il tier CDN. Le content delivery network (CloudFront, Cloudflare, Fastly, Akamai) mettono in cache le response su edge location vicine agli utenti. Il contenuto in cache è qualunque cosa sia per lo più statica: pagine HTML, immagini, bundle JavaScript, CSS, segmenti video, file di font, response API con TTL lunghi. Le cache hit del CDN non toccano mai gli origin server; vengono servite da un nodo geograficamente vicino all’utente, in decine di millisecondi anziché centinaia. Il tier CDN è la cache più veloce perché è la più vicina al consumatore.
L’economia del tier CDN è favorevole. Come trattato nella lezione 68, l’egress del CDN è tipicamente più economico dell’egress diretto dall’origin, e il rapporto cache-hit riduce ulteriormente il carico sull’origin. Un team che mette un CDN davanti agli asset statici tipicamente vede sia bollette più basse che latenze più basse, senza alcun costo di consistency (gli asset sono immutabili o hanno regole di rivalidazione ben comprese).
Il tier di application cache. Redis o Memcached, posizionati tra l’applicazione e il database. L’applicazione controlla prima la cache; su un hit, restituisce il valore in cache; su un miss, interroga il database e (nella maggior parte dei pattern) popola la cache per il chiamante successivo. L’application cache è il cavallo da tiro: mette in cache risultati di query costose, valori calcolati, dati di sessione, feature flag, contatori di rate-limit, leaderboard, e la coda lunga di “cose che l’applicazione legge più spesso di quanto scriva”.
Redis domina questo tier nel 2026. Memcached è ancora in giro, più semplice e più veloce per il caso puro key-value, ma le strutture dati più ricche di Redis (sorted set, stream, hyperloglog, pub/sub) e le sue opzioni di persistenza ne hanno fatto la scelta predefinita. Le offerte managed (ElastiCache, MemoryDB, Upstash, Redis Cloud) gestiscono la tassa operativa di farlo girare.
Il tier di database cache. Questo è spesso invisibile. Postgres ha una query result cache per i prepared statement; il buffer pool mette in cache le pagine lette di recente; le materialised view mettono in cache risultati pre-calcolati. MySQL ha il proprio buffer pool e (fino alla 8.0) aveva una query cache. Il tier di database cache è il più vicino ai dati e il più limitato in dimensione, ma per il workload che il database stesso sta gestendo, queste cache stanno facendo lavoro vero senza che il team applicativo debba pensarci.
I tre tier formano una gerarchia. Una request che colpisce il CDN non raggiunge mai l’applicazione; una request che colpisce l’application cache non raggiunge mai il database; una request che colpisce il buffer pool del database non raggiunge mai il disco. Ogni layer assorbe traffico che il successivo dovrebbe altrimenti gestire. Il lavoro architetturale è scegliere cosa vive a quale layer e come ogni layer rimane consistente con la sorgente.
I quattro pattern canonici
Una volta che una cache esiste, la domanda è come tenere i valori in cache consistenti con la sorgente sottostante. Quattro pattern sono il canone da manuale. Ciascuno fa un trade-off diverso tra semplicità, consistency e write throughput.
Cache-aside, anche detto lazy loading. L’applicazione è responsabile sia delle letture che delle scritture. Su una lettura, l’applicazione controlla prima la cache. Su un hit, restituisce il valore in cache. Su un miss, interroga il database, popola la cache con il risultato, e restituisce. Su una scrittura, l’applicazione scrive nel database e o invalida l’entry in cache o la aggiorna. Questo è il pattern più comune nei sistemi reali e quello che gli esempi di Redis dimostrano più spesso.
I vantaggi: la cache è un layer separato e opzionale; il codice applicativo è in controllo; il modo di fallire è graceful (un disservizio della cache significa letture più lente, non letture rotte). Gli svantaggi: c’è una finestra tra la scrittura nel database e l’invalidazione della cache in cui può essere letto un valore stale; il codice applicativo ha la logica della cache intrecciata al suo interno; i cache miss pagano il costo pieno del database.
Read-through. La cache fa fetch dal database in modo trasparente. L’applicazione chiede alla cache una key; la cache, su un miss, fa fetch dal database, si popola, e restituisce. Il codice applicativo non vede direttamente la chiamata al database; la cache la astrae.
Il vantaggio: meno codice di gestione della cache nell’applicazione. Lo svantaggio: accoppiamento più stretto tra cache e database; la cache deve sapere come leggere da ogni sorgente di dati che mette in cache; i modi di fallire sono più torbidi (un fallimento della cache diventa un fallimento del database dal punto di vista dell’applicazione). Il read-through è più comune nelle librerie di caching che si trovano al data-access layer (alcuni ORM, alcune sidecar cache) che nei deployment Redis grezzi.
Write-through. L’applicazione scrive nella cache, e la cache scrive nel database in modo sincrono. Entrambi vengono aggiornati prima che la scrittura sia confermata. La consistency forte è preservata: una lettura dopo una scrittura vede il nuovo valore o in cache o nel database, mai quello vecchio.
Il vantaggio: consistency. Lo svantaggio: la write latency è la somma della scrittura in cache e della scrittura nel database, più lenta di entrambe da sole. Il write-through è appropriato per workload read-heavy in cui la consistency è critica e il write throughput non è il collo di bottiglia.
Write-behind, anche detto write-back. L’applicazione scrive nella cache; la cache conferma immediatamente e fa flush al database in modo asincrono. Il pattern di scrittura più veloce, perché l’applicazione vede solo la latenza della cache.
Il vantaggio: write throughput. Lo svantaggio: se la cache crasha prima che il flush si completi, i dati vengono persi. Il write-behind è appropriato per workload che tollerano occasionale perdita di dati (incrementi di contatori, eventi di analytics, click tracking) e inappropriato per qualunque cosa dove una scrittura mancante sia un problema di correttezza.
Una precisazione pratica: i sistemi reali mescolano questi. Un dato deployment Redis potrebbe mettere in cache un set di key in cache-aside, un altro set di key in write-through, e un terzo set come buffer write-behind per un event log. I pattern non sono scelte esclusive per l’intero sistema; sono strumenti per use case.
Il problema dell’invalidazione
La battuta di Phil Karlton secondo cui “ci sono solo due cose difficili in computer science: l’invalidazione della cache e dare nomi alle cose” è un meme ma la metà sull’invalidazione della cache è genuina. Il problema è che la cache contiene una copia di dati che la sorgente potrebbe cambiare, e ci deve essere un meccanismo che riallinea la cache. I meccanismi esistono su uno spettro che va da “facile e lossy” a “difficile e corretto”.
Scadenza basata su TTL. Ogni valore in cache è memorizzato con un time-to-live (60 secondi, 5 minuti, 1 ora). Quando il TTL si esaurisce, la cache scarta il valore, e la lettura successiva ripopola dalla sorgente. Il pattern più semplice, e quasi sempre quello da cui partire.
Il trade-off è una staleness accettabile: tra una scrittura nel database e la successiva scadenza del TTL, la cache serve un valore stale. Per molti use case (una leaderboard, una lista di categorie, un feed di homepage) questo va bene. Per altri (un saldo del conto, un controllo di permessi) no.
Invalidazione esplicita. Quando l’applicazione scrive nel database, comunica anche alla cache che una particolare key non è più valida. La cache fa evict dell’entry; la lettura successiva ripopola. Il meccanismo è o una chiamata diretta di delete sulla cache o un messaggio publish-subscribe a cui qualunque caching layer è iscritto.
Il vantaggio è la correttezza: una scrittura è seguita da un’invalidazione, e le letture successive vedono il nuovo valore. Lo svantaggio è che ogni code path che scrive nel database deve ricordarsi di invalidare, e la superficie per i bug è grande. Una singola chiamata dimenticata è un bug di stale-cache che può persistere per la durata di vita dell’entry in cache.
Cache versioning. Invece di invalidare singole key, si incrementa una version key. Ogni entry in cache ha uno stamp di versione; in lettura, la cache controlla se la versione dell’entry corrisponde alla version key corrente; in caso contrario, l’entry è trattata come un miss. Per “invalidare tutto ciò che è taggato con category 5”, si incrementa la version key per category 5; tutte le entry in cache da prima dell’incremento sono ora stale.
Il pattern è utile per invalidare gruppi di entry difficili da enumerare. Il costo è una lettura della version key su ogni lettura della cache, che è economica se le version key stesse sono in cache localmente.
Invalidazione event-driven. Uno stream di change-data-capture dal database (territorio della lezione 46) alimenta un topic Kafka; i servizi di caching si iscrivono e invalidano le key affette quando arriva una modifica rilevante. Questo è il pattern architetturalmente più pulito: la sorgente di verità emette eventi di cambiamento, ogni consumer interessato (cache, indici di ricerca, repliche downstream) ascolta, e l’invalidazione è automatica.
Il costo è l’infrastruttura: una pipeline CDC, un topic, il plumbing dei consumer. Per un sistema che già fa girare CDC per altre ragioni, il costo marginale di alimentare la cache è basso. Per un sistema senza CDC, costruirla specificamente per l’invalidazione della cache raramente vale la pena.
La raccomandazione pragmatica è a strati. Usa il TTL come baseline predefinita (alla fine corregge ogni inconsistenza). Aggiungi invalidazione esplicita per le key la cui staleness è genuinamente dannosa. Ricorri a invalidazione CDC-driven quando il team ha già l’infrastruttura e i bisogni di correttezza del workload lo giustificano.
La cache stampede
Il singolo disservizio cache-related più comune nei sistemi in produzione è la cache stampede, anche detta dogpile o thundering herd. Il pattern è abbastanza consistente fra le aziende da avere il proprio folklore.
Lo scenario. Una hot key (il feed dell’homepage, il listing di prodotto popolare, il contatore globale) è in cache con un TTL. Il TTL si esaurisce. In quel preciso momento, centinaia o migliaia di request concorrenti stanno leggendo la key. Ogni request fa miss sulla cache. Ogni request interroga il database. Il database, che era dimensionato per il carico post-cache, vede uno spike improvviso di query identiche da ogni reader concorrente simultaneamente. Si satura. La latenza va a secondi. Le connessioni si accumulano. Il tier applicativo va in timeout. La cache alla fine viene ripopolata, ma da qualcosa che arriva via paging dell’on-call.
La causa radice è un fallimento di coordinazione. Non c’è alcun protocollo che dica “se molti reader fanno miss simultaneamente, solo uno dovrebbe ripopolare”. Il comportamento predefinito è “tutti ripopolano indipendentemente”.
Le mitigazioni sono ben note e vale la pena implementarle per qualunque cache key con traffico serio.
Single-flight refresh, anche detto request coalescing. Quando avviene un cache miss, viene preso un lock sulla key. Il reader che detiene il lock fa fetch dal database e popola la cache. Altri reader che fanno miss sulla stessa key aspettano brevemente che il lock venga rilasciato, poi leggono dalla cache ora popolata. Solo una query al database avviene per evento di miss indipendentemente dalla concorrenza. Esistono implementazioni per Redis (usando SET NX come primitiva di lock), per i framework applicativi (il package singleflight di Go, Caffeine di Java), e per le librerie di caching in generale.
Refresh probabilistico anticipato. Prima che il TTL si esaurisca completamente, un controllo probabilistico fa il refresh del valore. La probabilità aumenta man mano che il TTL si avvicina alla scadenza. Con questo pattern, la cache si ripopola su qualche lettura sfortunata vicino alla scadenza invece di aspettare che tutti facciano miss simultaneamente. Il paper originale è Vattani et al., “Optimal Probabilistic Cache Stampede Prevention” (PVLDB 2015).
Background refresh. Un job schedulato fa refresh del valore in cache prima che il TTL si esaurisca, indipendentemente da chi lo stia richiedendo. Le hot key non scadono mai dal punto di vista dei reader. Il pattern è appropriato per hot key prevedibili (l’homepage, la leaderboard, l’aggregato giornaliero) e inappropriato per la coda lunga (milioni di key per-utente sarebbero impraticabili da rinfrescare a schedule).
Stale-while-revalidate. Un’entry in cache oltre il suo TTL viene comunque restituita al reader, mentre un processo in background la rinfresca. Il reader ottiene un valore leggermente stale; la cache rimane calda. I CDN lo implementano nativamente (la direttiva stale-while-revalidate di CloudFront, una feature simile di Fastly). Le application cache possono implementarlo manualmente.
flowchart TD
R1[Reader 1] --> C{Cache lookup}
R2[Reader 2] --> C
R3[Reader 3] --> C
C -->|hit| HIT[Return cached value]
C -->|miss| LOCK{Acquire single-flight lock}
LOCK -->|got lock| DB[(Database query)]
LOCK -->|waiting| WAIT[Wait briefly]
DB --> POP[Populate cache, release lock]
POP --> HIT
WAIT --> C
Diagramma da creare: una versione animation-friendly che mostra lo scenario di stampede sulla sinistra (ogni reader colpisce il database simultaneamente quando il TTL scade) e il pattern single-flight sulla destra (un reader riempie la cache mentre gli altri aspettano). Il punto visivo è l’asimmetria fra i due: il carico sul database a sinistra raggiunge un picco proporzionale al numero di reader concorrenti; a destra, è costante indipendentemente dalla concorrenza.
Dimensionamento e eviction della cache
Una cache è per definizione più piccola della sorgente che mette in cache. Quando la cache è piena, qualcosa deve cedere. La policy di eviction decide cosa.
LRU (least recently used). Fa evict dell’entry che non è stata letta per il tempo più lungo. Il default per la maggior parte delle cache; matcha il caso comune in cui letture recenti predicono letture in un futuro vicino. Implementato nativamente in Redis (maxmemory-policy allkeys-lru) e Memcached.
LFU (least frequently used). Fa evict dell’entry con il minor numero di accessi. Migliore per workload con hot key che dovrebbero rimanere indipendentemente dall’accesso recente. L’opzione di Redis è allkeys-lfu.
Random. Fa evict di un’entry scelta a caso. Sorprendentemente competitivo con LRU in alcuni workload, ed economico da implementare. allkeys-random di Redis.
Basato su TTL. Fa evict dell’entry che scade prima. Utile quando le entry hanno TTL significativi e la cache dovrebbe essere mantenuta fresca.
La scelta raramente conta nello stato stazionario, ma la scelta sbagliata produce eviction correlate che sembrano fallimenti della cache. Un workload con hot key fatte evict sotto LRU naive a causa di un breve flood di letture di cold key è una sorgente ricorrente di alert “la cache è degradata”.
Un esempio lavorato
Una tipica pagina prodotto e-commerce illustra come i layer si compongono.
Lo scaffold HTML (header, footer, navigazione, bundle JavaScript) è servito da CloudFront da edge location, con un TTL di un’ora e rivalidazione tramite invalidazione tag-based quando va in deploy un rilascio.
I dati del prodotto (nome, descrizione, URL delle immagini, prezzo base) sono fetchati dall’applicazione da Redis, che contiene la riga del prodotto chiavata per ID prodotto con un TTL di 60 secondi. Su un cache miss, l’applicazione interroga Postgres, popola Redis sotto un lock single-flight, e restituisce. Quando un merchandiser aggiorna la descrizione del prodotto, la pipeline di pubblicazione scrive su Postgres e invalida esplicitamente la key Redis.
Il pricing (che dipende dalla location dell’utente, dalla currency, e dalle promozioni attive) è troppo dinamico per mettere in cache il risultato, ma gli input (la lista delle promozioni, il tasso di cambio) sono essi stessi messi in cache al tier applicativo con TTL più brevi e invalidazione esplicita quando una promozione viene attivata.
Le recensioni (coda lunga di traffico in lettura, contenuto che cambia lentamente) sono in cache sia al tier applicativo che al tier CDN, con il CDN che serve la maggior parte del traffico e il tier applicativo che fa da backstop per i cache miss.
L’inventory (conteggio delle unità rimanenti) è letto direttamente da Postgres senza caching, perché il costo della staleness è troppo alto (oversell costa più del carico sul database) e il pattern di lettura è moderato. Un team diverso potrebbe metterlo in cache con un TTL di 1 secondo, accettando una breve inaccuratezza come prezzo per il carico ridotto.
La forma dell’esempio è la forma di ogni architettura di caching reale: key diverse a layer diversi con policy diverse, scelte per use case in base al costo della staleness, al carico di lettura, e al costo di un miss.
Cosa prepara questa lezione
Il Modulo 9 riguarda l’ottimizzazione dei costi, e il caching è una delle sue leve più grandi. Un team che fa caching bene paga meno capacità di database, meno compute, meno egress di rete, e nel complesso meno infrastruttura sensibile alla latenza. Le prossime lezioni del Modulo 9 trattano lo storage layout, l’ottimizzazione delle query, e la disciplina FinOps che rende il costo una preoccupazione ingegneristica di prima classe. Il caching è la prima di queste leve, la più visibile architetturalmente, e quella più probabile a comporsi su anni di operatività.
Riferimenti e approfondimenti
- Andrei Vattani, Flavio Chierichetti, Keegan Lowenstein, “Optimal Probabilistic Cache Stampede Prevention”, PVLDB 2015,
http://www.vldb.org/pvldb/vol8/p886-vattani.pdf(consultato 2026-05-01). Il paper che ha formalizzato l’approccio probabilistic early-refresh. - Documentazione Redis, “Eviction policies”,
https://redis.io/docs/latest/operate/oss_and_stack/management/config/(consultato 2026-05-01). Il riferimento di configurazione per le policy discusse sopra. - Documentazione Redis, “Distributed locks with Redis”,
https://redis.io/docs/latest/develop/use/patterns/distributed-locks/(consultato 2026-05-01). Il riferimento per il pattern single-flight a livello Redis. - Wiki di Memcached,
https://github.com/memcached/memcached/wiki(consultato 2026-05-01). Per il cugino più semplice di Redis e la baseline ancora rilevante per il caching key-value grezzo. - AWS, “ElastiCache for Redis caching strategies”,
https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/Strategies.html(consultato 2026-05-01). Una walkthrough pratica dei quattro pattern canonici in un contesto AWS managed. - Blog di Cloudflare, “Cache stampede protection”,
https://blog.cloudflare.com/(consultato 2026-05-01). Scrittura dal punto di vista del vendor su stale-while-revalidate e pattern di edge cache. - La citazione “two hard things” di Phil Karlton è ampiamente attribuita; il contesto originale dell’era Netscape è documentato nel wiki c2 e nel bliki di Martin Fowler,
https://www.martinfowler.com/bliki/TwoHardThings.html(consultato 2026-05-01). - Michael Nygard, “Release It!”, seconda edizione (Pragmatic Bookshelf, 2018). Il pattern stampede è trattato sotto la più ampia categoria di “stability patterns”, insieme ai pattern bulkhead e circuit-breaker rilevanti per la lezione 69.