laranevans.com
Topics / Caching / Caching patterns

A cache keeps a copy of data somewhere fast so you do not pay the full cost of fetching it again. Reading from memory is orders of magnitude faster than reaching across a network to a database, and that speed gap is why caches exist. The moment you add one, the same piece of data lives in two places, and you have to decide how those two copies stay in step.

Every caching pattern answers two decisions. Make both, and the cache's behavior is settled.

Cache-aside: the application owns the cache

Cache-aside, also called look-aside, puts the application in charge of both the cache and the database. On a read, the application checks the cache first. On a miss, it reads from the database and then writes that value back into the cache, so the next read finds it (Microsoft's cache-aside reference, which AWS calls lazy loading).

Two properties follow. The cache fills only with data someone has requested, so you never spend memory on values nobody reads. And the cache stays an optimization rather than a dependency: if a cache node fails, reads slow down instead of failing.

The cost appears on a miss. A miss takes three trips before the application has its answer: cache, then database, then cache again (AWS). That cost scales well in practice. Facebook ran its memcache tier as a look-aside cache and pushed it to billions of requests per second (Nishtala et al.).

sequenceDiagram
    participant App as Application
    participant Cache
    participant DB as Database
    App->>Cache: read key
    alt cache hit
        Cache-->>App: value
    else cache miss
        Cache-->>App: miss
        App->>DB: read key
        DB-->>App: value
        App->>Cache: write value
    end

Read-through: the cache owns the database read

Read-through moves the database read that cache-aside leaves to the application into the cache itself. The application reads only from the cache. On a miss, the cache loads the value from the backing store on its own (Hazelcast's read-through loader).

The application code stays simpler, because no call site has to remember the load-on-miss logic. You trade that simplicity for coupling. The cache now has to know how to query your store, so the loading logic lives beside the cache instead of in your application. The complexity moves rather than disappears.

sequenceDiagram
    participant App as Application
    participant Cache
    participant DB as Database
    App->>Cache: read key
    alt cache hit
        Cache-->>App: value
    else cache miss
        Cache->>DB: read key
        DB-->>Cache: value
        Cache-->>App: value
    end

Write-through: cache and database commit together

With the read path settled, the question becomes when a write reaches the database.

Write-through writes every change to the cache and the database together, and the write is not finished until both have stored it (Hazelcast, AWS). Because the cache is never older than the database, a read that follows a write returns the new value. You pay for that guarantee in two ways. Each write is slower because of the second hop, and the cache fills with written data nobody reads again unless you add a time-to-live (TTL) to expire it (AWS).

sequenceDiagram
    participant App as Application
    participant Cache
    participant DB as Database
    App->>Cache: write value
    Cache->>DB: write value
    DB-->>Cache: ack
    Cache-->>App: ack

Write-back: cache now, database later

Write-back, also called write-behind, makes the opposite trade from write-through. It writes to the cache, acknowledges the write right away, and writes through to the database later in the background, often folding several updates to the same key into a single database write (Hazelcast).

Writes become fast and absorb bursts, because the slow database write is off the critical path. The risk is durability. If the cache crashes before it flushes, the changes that had not yet reached the database are lost.

sequenceDiagram
    participant App as Application
    participant Cache
    participant DB as Database
    App->>Cache: write value
    Cache-->>App: ack
    Note over Cache,DB: later, batched, in the background
    Cache->>DB: flush values

Write-around: write the database, skip the cache

Write-around sends writes straight to the database and does not touch the cache. The cached copy fills only later, when a read misses and the read path repopulates it. Writes avoid evicting cache entries nobody will read back, and in exchange the first read after a write is a guaranteed miss. Cache-aside's write path is write-around: update the database, then leave the cache to repopulate on demand.

sequenceDiagram
    participant App as Application
    participant Cache
    participant DB as Database
    App->>DB: write value
    DB-->>App: ack
    Note over Cache: cache untouched, fills on a later read miss

Choosing a pattern follows from the two decisions

Because a pattern is a read path paired with a write path, the choice comes apart into the same two decisions.

Match the read path to who should own database access. Choose read-through when you want application code to stay simple and the cache to own loading. Choose cache-aside when you want the application in control and the cache to remain a removable optimization.

Match the write path to how fresh your reads must be. Strong read-after-write freshness points to write-through. Write-heavy or bursty work that tolerates a small window of possible loss points to write-back. Work that rarely re-reads what it wrote points to write-around.

How a write clears or refreshes the cached value is a separate subject, cache invalidation, and it carries more failure modes than the write path alone.

The patterns at a glance

Pattern Decision What happens Main trade
Cache-aside (look-aside) Read App checks the cache, loads from the DB on a miss, backfills the cache Three trips on a miss. Caches only requested data
Read-through Read App reads only the cache. The cache loads from the DB on a miss Simpler application, but the cache couples to the store
Write-through Write Write to cache and DB together, synchronously Fresh reads, slower writes
Write-back (write-behind) Write Write to cache, acknowledge, persist to the DB later Fast writes, risk of loss on a crash
Write-around Write Write to the DB only. The cache fills on a later read miss No wasted cache writes, but a guaranteed first-read miss