Caching Strategies Explained: Cache-Aside, Read-Through, Write-Through and Write-Back

Four caching strategies side by side: who fills the cache, who writes the database, what can go stale, and how to show each one on an architecture diagram.

· Founder & developer

Published

Caching Strategies Explained: Cache-Aside, Read-Through, Write-Through and Write-Back

On most architecture diagrams a cache is one box between a service and its database. That box hides the questions that decide whether the cache helps or hurts: who puts data into it, who writes to the database, and what a reader sees in between.

Four strategies answer those questions differently: cache-aside, read-through, write-through and write-back. This guide walks through each one with the same example, compares them, covers the failures that show up in production, and ends with a diagram you can reuse.

Two questions every cache answers

Every caching strategy is a pair of answers:

  1. Who loads data into the cache on a read? Either your application code or a cache layer that knows how to reach the database.
  2. Who writes to the database, and when? Your application, the cache layer in the same request, or the cache layer later.
Strategy Read: who fills the cache Write: who writes the database
Cache-aside The application, after a miss The application, then it invalidates the key
Read-through The cache layer, after a miss Not defined by the strategy; usually paired with one of the next two
Write-through Usually combined with read-through or cache-aside The cache layer, in the same request
Write-back (write-behind) Usually combined with read-through The cache layer, later and often in batches

The running example is a product API with a Postgres products table and a Redis cache. A product page reads product:42, and an admin tool updates prices.

Cache-aside (lazy loading)

The application talks to both stores. It asks the cache first; on a miss it reads the database and puts the result into the cache with a time to live (TTL). AWS's ElastiCache documentation describes the same pattern as lazy loading.

def get_product(product_id):
    key = f"product:{product_id}"
    cached = redis.get(key)
    if cached is not None:
        return json.loads(cached)                  # hit
    row = db.fetch_one("SELECT * FROM products WHERE id = %s", product_id)
    if row is not None:
        redis.set(key, json.dumps(row), ex=300)    # fill, with a 300 s TTL
    return row

def update_price(product_id, price):
    db.execute("UPDATE products SET price = %s WHERE id = %s", price, product_id)
    redis.delete(f"product:{product_id}")          # invalidate, don't overwrite

On writes, delete the key instead of writing the new value into the cache. The next read loads the current row. Microsoft's cache-aside pattern describes the same write rule.

What you get: only data someone asked for is cached, and if the cache is down, reads still work by going to the database, just slower. What you pay: every miss costs three round trips (cache, database, cache), and there is a race. A reader that missed can read the old row, the writer updates the row and deletes the key, and then the reader writes the old value back. It stays stale until the TTL ends, which is one reason cache-aside always needs a TTL.

Read-through

The application only calls the cache. On a miss, the cache layer loads the value from the database through a loader you configure, stores it and returns it. The read result is the same as cache-aside, but the loading code lives in one place instead of in every caller.

A plain Redis server does not read from your database on its own. Read-through comes from a layer that can: a caching library with a loader function, a proxy, or a managed cache built for one database. Amazon DynamoDB Accelerator (DAX) is an example: on a cache miss it passes the read to DynamoDB, returns the result and caches it.

The trade-off: the cache layer is now on the read path. If it fails, reads fail unless the client has a fallback to the database.

Write-through

The application writes to the cache layer, and the layer writes the database in the same operation. The application gets a success only when both writes succeed. DAX documents exactly this: data is written to the DynamoDB table first, then to the cache, and the call succeeds only if both writes do.

What you get: data written through the cache is current in the cache, so a read right after a write sees it. What you pay: every write waits for two stores, and the cache fills with data that is written but never read. Add a TTL so that data expires. A new, empty cache node also has nothing until data is written again, which is why write-through is usually combined with read-through or lazy loading.

Write-back (write-behind)

The application writes to the cache and gets a success right away. The cache layer writes to the database later, often in batches, and repeated updates to the same key collapse into one database write.

What you get: the lowest write latency and far fewer database writes during bursts. What you pay: anything not yet flushed is lost if the cache node fails, unless the buffer is replicated or backed by a durable log. The database is also behind the cache, so reports, other services and backups see older data. Write-back fits view counters and similar data where losing a few seconds is acceptable.

Side by side

Cache-aside Read-through Write-through Write-back
App code knows the database Yes No No No
Write latency Database write plus a cache delete Depends on the write strategy Cache and database Cache only
Stale reads Until invalidation or TTL Until invalidation or TTL Rare for writes that go through the cache The database lags the cache
Cache node lost Slower reads Reads fail without a fallback Missing data until rewritten Unflushed writes are lost
Typical fit The default with Redis A library or managed cache Read-after-write freshness Write-heavy counters and bursts

TTLs, eviction and three classic failures

A TTL bounds how stale a value can get. Pick it per kind of data from the staleness the product accepts: a product description can live for minutes, a stock level maybe seconds. When an expired key is read, it behaves as if it were not there.

Eviction is a separate mechanism: what the cache removes when it runs out of memory. In Redis, maxmemory sets the limit and maxmemory-policy picks what to remove. The default in redis.conf is noeviction, which returns errors on commands that add data once memory is full. For a pure cache, the Redis documentation suggests allkeys-lru as a good default. Managed services set their own defaults, so check yours. Measure the hit ratio from INFO stats as keyspace_hits / (keyspace_hits + keyspace_misses).

Three failures show up in almost every cache:

  • Stampede. A hot key expires and hundreds of requests miss at the same moment, all querying the database. Let one request rebuild the value and make the others wait briefly or serve the old value. A Redis lock does this: SET lock:product:42 <token> NX PX 5000 succeeds for one caller only. Facebook's memcache paper solves the same problem with leases and reports that, for keys prone to it, the peak database query rate fell from 17K/s to 1.3K/s.
  • Penetration. Requests for IDs that do not exist always miss and always hit the database. Cache the "not found" result with a short TTL, or check a Bloom filter of valid IDs first.
  • Avalanche. Many keys expire together, for example after a bulk load or a cache restart, and the database takes the full load at once. Add random jitter to TTLs, such as 300 seconds plus up to 60, and warm the cache gradually.

The diagram

Four rows with the same three participants, so the difference is in the edges: who talks to the database, and when.

Four rows, each with a product API, a cache and a Postgres database. In cache-aside the API reads the cache, then the database, then fills the cache. In read-through and write-through the cache layer talks to the database. In write-back the cache flushes writes later.

20 components · 9 connections

Open in Archboard
Diagram contents
  • Product API
  • Redis
  • Postgres
  • Cache layer
  • 1 GET (miss)
  • 2 SELECT
  • 3 SET, TTL 300 s
  • 1 read
  • 2 on miss: SELECT
  • 1 write
  • 2 UPDATE, same request
  • 1 write, OK at once
  • 2 flush later, batched
The four strategies from this guide for one product API: who reads and writes the database in each, what the app waits for, and what goes wrong when the cache fails or falls behind.

How to read it:

  • Row 1, cache-aside. The product API reads Redis (1), reads Postgres itself on a miss (2) and fills Redis with a TTL (3). The edge to Postgres bypasses the cache: the application owns both calls.
  • Row 2, read-through. The API only talks to the cache layer, which loads from Postgres on a miss.
  • Row 3, write-through. The write goes through the cache layer to Postgres in the same request.
  • Row 4, write-back. The API gets its success at once. The dashed edge is the later, batched flush.

How to draw caches on architecture diagrams

  • Name the strategy and the TTL. "Cache-aside, TTL 300 s" on the cache node or its edge answers the first question a reviewer asks.
  • Draw who talks to the database. In cache-aside, the service has an edge to the database as well as to the cache. A cache drawn inline in front of the database suggests read-through, and readers will assume it.
  • Draw the invalidation path. Show who deletes keys, including batch jobs and admin tools that write the database directly. A writer with no path to the cache is where stale data comes from.
  • Write down the failure behavior. "Cache down: reads go to Postgres" tells the on-call engineer more than a second cache icon.
  • Put the numbers on the canvas. Hit-ratio target, memory limit and eviction policy fit in a small note next to the cache.
  • Dash anything asynchronous. A write-back flush or an invalidation event is not part of the request.

Choosing

Start with cache-aside and a TTL. It works with any cache and any database, and it degrades to slower reads when the cache fails. Move to read-through when a library or managed cache gives you a loader. Add write-through when reads right after writes must see the new value and writes can afford the extra latency. Use write-back only when you can lose or replay unflushed writes.

For the picture around the cache, see the system design diagram example, where Redis serves the read path of a URL shortener, and the microservices diagram rules for showing which service owns which data.

Build it in Archboard

  1. Sign in with Google or GitHub and create a diagram.
  2. For each strategy, add a Core Text title, then put three nodes in a row: a C4 Model Container for the product API, a Data pack Redis for the cache and a Data pack Postgres for the database. Label the cache "Cache layer" in rows 2 to 4.
  3. Connect each row with numbered edge labels: "1 GET (miss)", "2 SELECT" and "3 SET, TTL 300 s" for cache-aside, and the read or write steps for the other rows. Route the cache-aside edge from the API to Postgres over the cache.
  4. Set the write-back flush edge to dashed.
  5. Add a Core Text note under each row with the write rule and what happens when the cache fails.

The example uses 12 diagram nodes, within the Free plan. For the general method behind any architecture diagram, see how to draw a software architecture diagram.

FAQ

Is cache-aside the same as lazy loading? Yes. Both names describe the application loading data into the cache only after a miss. AWS documentation uses "lazy loading"; many other sources say "cache-aside".

Should I update or delete the cache key when data changes? Delete it in cache-aside. Two writers that each put their new value into the cache can finish in the wrong order and leave an old value behind. A delete followed by a fresh load avoids that, and the TTL limits the rarer race described above.

Can Redis do read-through or write-through by itself? No. A Redis server stores what clients send it. Read-through and write-through need a layer that knows your database: a caching library, a proxy, or a managed cache built for one database.

What TTL should I use? The longest staleness the product accepts for that kind of data. Then add jitter so that keys loaded together do not expire together.

Is write-back safe? Only if losing or replaying unflushed writes is acceptable, or if the write buffer is replicated or backed by a durable log. Avoid it for data such as payments or orders.

Start free — one seat, no credit card.

Related posts

← Back to blog

Open a canvas in a minute

Sign in with Google or GitHub. Free for a single seat. Upgrade when the team joins.