Caching Strategies Lesson 2 of 6 ⏱️ 5 min read

Cache-Aside, Read-Through, Write-Through & Write-Behind

The four core caching patterns compared, with Python examples and a decision table for when to use each.

Updated Oct 30, 2026

Four core caching patterns

How your application reads and writes through a cache determines its consistency, latency and failure behaviour. Four patterns cover almost every real system.

1. Cache-aside (lazy loading)

The application manages the cache itself:

  1. Look in the cache.
  2. On a miss, read from the database and store the result in the cache.
  3. On a write, update the database and delete the cache entry.

It's the most common pattern — and what most people mean by "adding Redis".

Python
import time

class FakeDB:
    """Stands in for a slow database and counts how often it's queried."""
    def __init__(self):
        self.rows = {1: {"id": 1, "name": "Intro to Python"}}
        self.queries = 0

    def get(self, key):
        self.queries += 1
        return self.rows.get(key)

    def put(self, key, value):
        self.rows[key] = value


class TTLCache:
    def __init__(self):
        self.store = {}

    def get(self, key):
        item = self.store.get(key)
        if item is None or item[1] < time.monotonic():
            return None
        return item[0]

    def set(self, key, value, ttl):
        self.store[key] = (value, time.monotonic() + ttl)

    def delete(self, key):
        self.store.pop(key, None)


db, cache = FakeDB(), TTLCache()

def get_course(course_id):
    key = f"course:{course_id}"
    course = cache.get(key)
    if course is None:                       # miss → load and populate
        course = db.get(course_id)
        cache.set(key, course, ttl=300)
    return course

def update_course(course_id, data):
    db.put(course_id, data)                  # 1. write the source of truth
    cache.delete(f"course:{course_id}")      # 2. invalidate (don't update) the cache

for _ in range(5):
    get_course(1)
print("db queries after 5 reads:", db.queries)

update_course(1, {"id": 1, "name": "Python for Beginners"})
print(get_course(1)["name"], "| db queries:", db.queries)

Output:

Plaintext
db queries after 5 reads: 1
Python for Beginners | db queries: 2

Five reads cost one query. After the update, the next read misses, reloads the new value, and the cache is warm again.

Why delete instead of update on writes? If two requests update the same item concurrently, "set cache" calls can arrive out of order and leave an old value cached. Deleting is simpler and self-healing.

Pros: only requested data is cached; a cache outage just makes things slower. Cons: the first request after a miss is slow; there's a short window where a stale value can be read.

2. Read-through

The cache itself loads from the database on a miss, using a loader function you provide. The application only talks to the cache. It's the same idea as cache-aside, moved into a library or caching layer — cleaner code, but you depend on the cache's loading behaviour.

3. Write-through

Every write goes to the cache and the database together, synchronously:

Python
def update_course_write_through(course_id, data):
    db.put(course_id, data)
    cache.set(f"course:{course_id}", data, ttl=300)

Pros: the cache is always warm and fresh for written data. Cons: every write pays for two operations; you may cache data that's never read; out-of-order concurrent writes need care (use versions, or fall back to invalidation).

4. Write-behind (write-back)

Writes go to the cache first and are flushed to the database later, often in batches.

Pros: extremely fast writes; batching reduces database load (great for counters, analytics, view counts). Cons: if the cache crashes before flushing, data is lost; harder to reason about. Use it only for data you can afford to lose or rebuild.

Choosing a pattern

Pattern Read latency Write latency Consistency Risk Typical use
Cache-aside Slow on miss Normal Good with invalidation Brief staleness Default choice for most apps
Read-through Slow on miss Normal Same as cache-aside Depends on library Caching frameworks, CDNs
Write-through Fast Slower Strong for cached keys Wasted cache space Read-after-write critical data
Write-behind Fast Fastest Eventual Data loss on failure Counters, metrics, logs

Eviction policies

When the cache is full, something must go:

  • LRU (Least Recently Used) — evict what hasn't been used longest. The most common default.
  • LFU (Least Frequently Used) — evict the least popular; good for stable popularity.
  • FIFO — evict the oldest insert; simple but ignores usage.
  • TTL-based — entries expire on schedule regardless of space.

Redis supports these via its maxmemory-policy setting (for example allkeys-lru). You'll build an LRU cache from scratch later in this tutorial.

Common mistakes

  • Updating the cache before the database — if the DB write fails, the cache holds data that never existed.
  • Using write-behind for important data like orders or payments.
  • Forgetting to cache misses ("not found"), so repeated lookups for missing IDs hit the database every time.
  • Inconsistent key naming — use a scheme like entity:id:version.

Key takeaways

  • Cache-aside is the default: read-through on miss, delete on write.
  • Write-through keeps caches fresh at the cost of slower writes.
  • Write-behind is fastest but can lose data — only for non-critical writes.
  • Combine every pattern with TTLs and a sensible eviction policy.
Finished this lesson?
Tick it off to track your Caching Strategies progress.
Next lesson