Caching is introduced because hitting the origin is expensive. But the decision to cache doesn’t end there. Two questions immediately follow — on a miss, who fills the cache, and where do writes go?
The five canonical cache patterns (Cache-aside, Read-through, Write-through, Write-back, Refresh-ahead) are combinations of choices along those two axes. Whether the application or the cache itself fills on a miss; whether writes go through the cache, bypass it, or flow asynchronously. Separating these two axes makes pattern selection follow naturally.
Cache-aside
The most common pattern. The application queries the cache directly. On a miss, the application reads from the origin and then populates the cache. Writes go straight to the origin; the application is responsible for invalidating or refreshing the cache.
flowchart LR
APP["App"] -->|"1. lookup"| C["Cache"]
C -->|"2. miss"| APP
APP -->|"3. fetch"| DB[("Origin")]
DB -->|"4. data"| APP
APP -->|"5. populate"| C
The application owns the responsibility of filling the cache. Implementation is simple, but consistency responsibility ends up scattered across application code. The stale window between cache and origin has to be managed by the application, and every write decides whether to invalidate or refresh the cache.
Read-through
The cache owns filling responsibility. The application only calls the cache. On a miss, the cache reads from the origin, populates itself, and returns the value to the application.
flowchart LR
APP["App"] -->|"1. lookup"| C["Cache"]
C -->|"2. miss → fetch"| DB[("Origin")]
DB -->|"3. data"| C
C -->|"4. data"| APP
The difference from Cache-aside is exactly where the filling responsibility lives. Application code becomes simpler, but the cache now needs to know about the origin. An origin loader function is typically registered with the cache so the cache can call the origin on its own. The coupling between the cache layer and the data layer is tighter than with Cache-aside.
Write-through
Writes update the cache and the origin synchronously. The application sends the write to the cache, which then updates the origin before acknowledging.
flowchart LR
APP["App"] -->|"1. write"| C["Cache"]
C -->|"2. write"| DB[("Origin")]
DB -->|"3. ack"| C
C -->|"4. ack"| APP
The cache and origin always agree. Strong consistency comes naturally. The cost: write latency now includes both the cache write and the origin write. When combined with Read-through, both read and write paths go through the cache. The same combination also tends to pollute the cache with rarely read entries (cold-cache pollution), reducing hit rate.
Write-back
Writes are reflected synchronously in the cache, while the origin is updated asynchronously. The pattern is also known as write-behind.
flowchart LR
APP["App"] -->|"1. write"| C["Cache"]
C -->|"2. ack"| APP
C -.->|"3. async flush"| DB[("Origin")]
Write responses get faster. For write-heavy workloads, origin load drops significantly. The trade-off: cache-origin consistency becomes eventual, and any writes not yet flushed are lost if the cache dies. Durability reinforcements are typically added on the cache side — persistent queues, batch flushes, replication. The pattern fits domains that can tolerate some write loss.
Refresh-ahead
The cache proactively re-fetches entries that are about to expire. It’s a predictive approach that aims to reduce read misses outright.
flowchart LR
APP["App"] -->|"read"| C["Cache"]
C -->|"hit"| APP
C -.->|"async (near-expiry)"| DB[("Origin")]
DB -.->|"refresh"| C
Entries likely to be read get refreshed before they expire, so the application almost always sees a hit. The burst latency right after an expiration is avoided. The catch is needing to predict which entries are “likely to be read” — refresh is triggered by TTL (Time To Live) proximity or by an access-frequency heuristic. When the prediction misses, unnecessary origin calls accumulate.
Comparison Matrix
| Pattern | Read fill | Write path | Consistency | Suitable workload |
|---|---|---|---|---|
| Cache-aside | application | direct to origin (cache invalidation/refresh by app) | weak (app-managed) | read-heavy / varied write patterns |
| Read-through | cache | (combined with another pattern) | weak-to-moderate | read-heavy / simpler app code preferred |
| Write-through | (combined) | cache + origin synchronously | strong | consistency-critical / infrequent writes |
| Write-back | (combined) | cache sync + origin async | eventual | write-heavy / some loss tolerable |
| Refresh-ahead | cache (proactive) | (combined) | weak-to-moderate | read-heavy / predictable access pattern |
The read-side patterns (Cache-aside, Read-through, Refresh-ahead) and the write-side patterns (Write-through, Write-back) are orthogonal — they combine freely. Read-through + Write-through is the most common pairing; Refresh-ahead can be layered on top of any write pattern.
The two questions from the introduction — where does fill responsibility live, where do writes go — settle the design before pattern names enter the conversation. “Is it Cache-aside or Read-through?” only becomes a meaningful question after those two decisions are made.