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

PatternRead fillWrite pathConsistencySuitable workload
Cache-asideapplicationdirect to origin (cache invalidation/refresh by app)weak (app-managed)read-heavy / varied write patterns
Read-throughcache(combined with another pattern)weak-to-moderateread-heavy / simpler app code preferred
Write-through(combined)cache + origin synchronouslystrongconsistency-critical / infrequent writes
Write-back(combined)cache sync + origin asynceventualwrite-heavy / some loss tolerable
Refresh-aheadcache (proactive)(combined)weak-to-moderateread-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.