---
title: "12 — Caching Strategies: Cache-Aside, Write-Through, Write-Behind, Refresh-Ahead"
uid: caching-strategies
tags: ["write-through", "cache-aside", "write-behind", "refresh-ahead", "caching", "roadmap:system-design", "system-design"]
excerpt: "Each cache strategy is a different answer to who owns the write to the cache and when — trading read speed, write speed, staleness, and durability in specific ways."
date: 2026-08-13T03:27:32+0000
source: https://www.aveshina.my.id/en/blog/caching-strategies
---

"Just put Redis in front of the database" was my caching strategy, and it collapsed four different designs into one. Writing them down turned the choice into a precise trade-off: **each strategy is a different answer to who owns the write to the cache and when — the application on demand, or the cache itself on every write — and the choice trades read speed, write speed, staleness, and durability in specific, predictable ways.** [1][2] "Caching" is not one strategy; it is four, and the differences are where the engineering lives.

The framing that clicked is to read each strategy as a contract between the application, the cache, and the database. The contract says who checks the cache first, who is responsible for writing to it, whether the DB write is synchronous or asynchronous, and whether entries refresh before they expire. Once I could name those four contracts, picking one became a question about which property I cared about most.

## The four strategies

```figure
<svg viewBox="0 0 740 320" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Four caching strategies. Cache-Aside: app checks cache, on miss loads from DB and sets cache. Write-Through: app writes to cache, cache synchronously writes to DB. Write-Behind: app writes to cache, cache asynchronously writes to DB. Refresh-Ahead: cache proactively refreshes popular entries before expiry.">
  <defs>
    <marker id="cachearrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Cache-Aside -->
    <rect x="20" y="20" width="340" height="120" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="40" y="44" font-size="13" font-weight="700" fill="#1e1b4b">Cache-Aside (lazy loading)</text>
    <text x="40" y="60" font-size="10" fill="#475569">app owns the read; cache set on miss</text>
    <g font-family="ui-monospace, monospace" font-size="10" fill="#1e1b4b">
      <text x="40" y="84">1. app → cache.get(k)</text>
      <text x="40" y="99">2. miss → app → DB</text>
      <text x="40" y="114">3. app → cache.set(k, v)</text>
    </g>
    <text x="40" y="134" font-size="9" font-style="italic" fill="#475569">only requested data is cached</text>

    <!-- Write-Through -->
    <rect x="380" y="20" width="340" height="120" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="400" y="44" font-size="13" font-weight="700" fill="#052e16">Write-Through</text>
    <text x="400" y="60" font-size="10" fill="#475569">cache owns a synchronous DB write</text>
    <g font-family="ui-monospace, monospace" font-size="10" fill="#052e16">
      <text x="400" y="84">1. app → cache.set(k, v)</text>
      <text x="400" y="99">2. cache → DB (sync)</text>
      <text x="400" y="114">3. cache data is never stale</text>
    </g>
    <text x="400" y="134" font-size="9" font-style="italic" fill="#475569">slow writes, fast reads, no staleness</text>

    <!-- Write-Behind -->
    <rect x="20" y="160" width="340" height="120" rx="10" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="40" y="184" font-size="13" font-weight="700" fill="#422006">Write-Behind (write-back)</text>
    <text x="40" y="200" font-size="10" fill="#475569">cache owns an async DB write</text>
    <g font-family="ui-monospace, monospace" font-size="10" fill="#422006">
      <text x="40" y="224">1. app → cache.set(k, v)</text>
      <text x="40" y="239">2. cache → DB (async, later)</text>
      <text x="40" y="254">3. fast writes, risk of loss</text>
    </g>
    <text x="40" y="274" font-size="9" font-style="italic" fill="#475569">fastest writes; DB may lag the cache</text>

    <!-- Refresh-Ahead -->
    <rect x="380" y="160" width="340" height="120" rx="10" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="400" y="184" font-size="13" font-weight="700" fill="#500724">Refresh-Ahead</text>
    <text x="400" y="200" font-size="10" fill="#475569">cache refreshes popular entries pre-expiry</text>
    <g font-family="ui-monospace, monospace" font-size="10" fill="#500724">
      <text x="400" y="224">1. cache tracks popular keys</text>
      <text x="400" y="239">2. before TTL, cache → DB</text>
      <text x="400" y="254">3. user never waits for refresh</text>
    </g>
    <text x="400" y="274" font-size="9" font-style="italic" fill="#475569">low latency if predictions are accurate</text>
  </g>
</svg>
```

- **Cache-aside** (a.k.a. lazy loading). The application is responsible for reading and writing storage; the cache does not interact with storage directly. On a read, the app looks in the cache; on a miss, it loads from the database, adds the entry to the cache, and returns it [3]. Only the requested data is cached, so the cache is not filled with data nobody asks for. The cost: a cache miss is slow (two trips), and writes that bypass the cache can leave it stale until the entry expires or is evicted.
- **Write-through.** The application uses the cache as the main data store — it reads and writes to the cache — and the cache is responsible for _synchronously_ writing through to the database [1][2]. Data in the cache is never stale, because every write hits the DB before acknowledging. The cost: writes are slow (they wait for the DB), and newly created cache nodes (after a failure or scale event) start empty until entries are written.
- **Write-behind** (a.k.a. write-back). Same as write-through, except the cache writes to the database _asynchronously_, improving write performance [1][2]. The cost is real: if the cache goes down before its contents hit the data store, that data is lost. It is more complex to implement than cache-aside or write-through.
- **Refresh-ahead.** The cache is configured to automatically refresh recently-accessed entries _before_ they expire [4]. If the cache accurately predicts which items will be needed next, refresh-ahead yields lower latency than read-through, because the user never waits for a refresh. The cost: if the prediction is wrong, it does more work than not caching at all and can reduce performance.

## The trade-off map

The way of thinking I keep is a 2-axis map: who owns the write (application vs cache), and when the DB sees it (synchronously vs asynchronously / on-demand).

- **Application-owned, on-demand** (cache-aside): simplest, only caches what is asked for, but misses are slow and staleness is possible.
- **Cache-owned, synchronous** (write-through): no staleness ever, but every write pays the DB round-trip.
- **Cache-owned, asynchronous** (write-behind): writes are fast, but durability is at risk and the implementation is complex.
- **Cache-owned, predictive** (refresh-ahead): reads stay fast for hot keys, but burns resources if predictions miss.

## How I use this

Cache-aside is my default — it is the simplest strategy, it does not fill the cache with unrequested data, and it composes cleanly with anything. I move to write-through when staleness is genuinely harmful and the slower writes are acceptable (user profile updates, configuration). I reserve write-behind for the rare write-heavy case where I am willing to accept the durability risk in exchange for throughput, and only when I have a story for cache-crash recovery. Refresh-ahead I use only for a small set of predictably-hot keys where a miss would be user-visible. The discipline is to name the property I am optimizing for — simplicity, freshness, write throughput, or read latency — and pick the strategy whose contract delivers exactly that.

## References

[1] J. Bonér, "Scalability, availability, stability patterns," SlideShare, 2014. [Online]. Available: [https://www.slideshare.net/jboner/scalability-availability-stability-patterns/](https://www.slideshare.net/jboner/scalability-availability-stability-patterns/)

[2] T. Matyashovsky, "From cache to in-memory data grid — introduction to Hazelcast," SlideShare. [Online]. Available: [https://www.slideshare.net/tmatyashovsky/from-cache-to-in-memory-data-grid-introduction-to-hazelcast](https://www.slideshare.net/tmatyashovsky/from-cache-to-in-memory-data-grid-introduction-to-hazelcast)

[3] D. Martin, "Application caching — cache aside," system-design-primer (open source), 2024. [Online]. Available: [https://github.com/donnemartin/system-design-primer#application-caching](https://github.com/donnemartin/system-design-primer#application-caching)

[4] EnjoyAlgorithms, "Caching strategy: refresh-ahead pattern," 2023. [Online]. Available: [https://www.enjoyalgorithms.com/blog/refresh-ahead-caching-pattern](https://www.enjoyalgorithms.com/blog/refresh-ahead-caching-pattern)

[5] M. Moshikoo, "Caching strategies," Medium, 2022. [Online]. Available: [https://medium.com/@mmoshikoo/cache-strategies-996e91c80303](https://medium.com/@mmoshikoo/cache-strategies-996e91c80303)

```quiz
Q: In cache-aside, who is responsible for writing to the cache?
- the application, on demand (after a cache miss it loads from DB and sets the cache)
- the cache itself, synchronously on every write
correct: 0
explain: Cache-aside leaves read/write ownership with the application. On a miss, the app loads from the DB and populates the cache. The cache never talks to the DB directly.

Q: Which strategy guarantees the cache is never stale, at the cost of slower writes?
- write-behind
- write-through
correct: 1
explain: Write-through synchronously writes every update to the DB before acknowledging, so the cache is never stale. Writes pay the DB round-trip.

Q: The main risk of write-behind is…
- the cache can be stale after a write
- data loss if the cache crashes before flushing to the DB
correct: 1
explain: Write-behind writes to the DB asynchronously. If the cache goes down before its contents reach the DB, those writes are lost.

Q: Refresh-ahead improves latency only when…
- it accurately predicts which items will be needed before they expire
- it refreshes every item in the cache on every read
correct: 0
explain: Refresh-ahead proactively refreshes recently-accessed entries before expiry. If its predictions are wrong, it does wasteful work and can hurt performance.
```
