The main Redis caching patterns, how to expire and invalidate entries, and the trade-offs of each.
From Ultra Transcenders AI-200 by Tony Rough (publishing soon)
Redis doesn’t keep itself in step with your database; the application (or a supporting process) decides when to load, update and invalidate cached data. Choosing the right caching pattern is about how much staleness the data can tolerate and how much write-path complexity you will accept.
| Pattern | How it works | Freshness after a write | Good fit |
|---|---|---|---|
| Cache-aside (lazy loading) | Read cache; on a miss read the database, populate the cache with a TTL. On update, write the database then delete the key | A reader can briefly see a miss or stale data until the next read repopulates | Read-heavy data that tolerates short staleness, such as a product catalogue |
| Reference-data prefetching | Load small, stable data at start-up and refresh it when the source changes; often no TTL | Warm on every read once refreshed | Categories, lookup tables, configuration |
| Write-through | Update the database and the cache in the same operation; return only after both succeed | Readers see the new value immediately | Read-heavy paths that need read-after-write freshness, such as prices |
| Event-driven invalidation | Writers append change events (for example to a Redis stream); consumers update or delete cached entries | As fast as events are processed; survives consumer restarts | Derived values such as order status projections |
| Session and state offload | Transient state lives only in the cache | Not applicable | Carts, sessions, agent short-term memory |
The cache is an optimisation, never the source of truth. This sketch follows the Learn pattern: Figure 9.1 shows the read path and the order of operations on an update.
CACHE_TTL_SECONDS = 300
def get_product(product_id: int) -> dict | None:
key = f"product:{product_id}"
cached = r.get(key)
if cached is not None: # cache hit
return json.loads(cached)
product = load_product_from_db(product_id) # cache miss
if product is not None: # avoid caching null values
r.set(key, json.dumps(product), ex=CACHE_TTL_SECONDS)
return product
def update_product(product: dict) -> None:
save_product_to_db(product) # 1. update the source of truth
r.delete(f"product:{product['id']}") # 2. then invalidateproduct:42, user:1001:profile) and add a version when the value schema can change.Common trap: Invalidating the cache entry before writing to the database “to be safe” - that ordering opens a window where a concurrent reader caches the old value again; write the database first, then delete the key.
Common trap: Choosing cache-aside when users must see their own update immediately - cache-aside can serve a miss or stale value right after a write; use write-through for read-after-write freshness.
This note is one section of Ultra Transcenders AI-200: Developing AI Cloud Solutions on Azure, an independent study guide that explains every topic the exam covers by technology, with comparison tables, diagrams and the common traps, plus a glossary linked to Microsoft Learn.
Publishing soon on Amazon in Kindle and paperback editions.
About the book · AI-200 terms in the glossary · All AI-200 study notes
The five Cosmos DB consistency levels, their RU and latency trade-offs, and when to choose each.
How to tell commands, discrete events and telemetry streams apart and pick the right Azure messaging service.
How to define KEDA scalers for queues, topics and other event sources in Container Apps.
What creates a new revision, and how single and multiple revision modes change deployments.
Exact search versus approximate IVFFlat, HNSW and DiskANN indexes, and how to tune each for recall and latency.
How the Functions hosting plans differ in scaling, networking and cold start, and which to choose.
Control plane versus data plane, Azure RBAC versus vault access policies, and the roles apps need.