Redis Caching Patterns Every Backend Engineer Should Know
Dean Jain
Senior Staff Software Engineer · Enterprise AI, Data & Cloud Architect
· 5 min read
---
config:
theme: dark
fontSize: 17
themeVariables:
fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart LR
APP["🖥️ App"]:::obs <--> CACHE[("⚡ Redis cache<br/>(in-memory)")]:::gate
CACHE <--> DB[("🗄️ Source DB")]:::server
classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
classDef gate fill:#D7C3F2,stroke:#8E5BD0,stroke-width:2px,color:#0F172A
classDef server fill:#A8E6D0,stroke:#2FA37C,stroke-width:2px,color:#0F172A
Figure 1: A cache sits between the app and the source of truth but *how* reads and writes flow through it is the pattern, and that's what matters.
Adding Redis in front of your database is easy. Keeping that cache correct consistent with the source of truth, without serving stale data or losing writes is where the actual engineering lives. The decision isn’t “should I cache?” but “which caching pattern?” because cache-aside, read-through, write-through, and write-behind each make a different trade between consistency, latency, and complexity. Pick the wrong one and you get subtle staleness bugs or lost data. Here are the patterns every backend engineer should have at their fingertips.
Summary
- The pattern, not the cache, is the decision. Each one trades consistency vs. latency vs. complexity differently.
- Cache-aside (lazy loading) the default. App checks cache, falls back to DB on a miss, and populates the cache. Simple and resilient; first read is a miss.
- Read-through the cache itself loads from the DB on a miss, so the app only ever talks to the cache. Cleaner app code, more cache responsibility.
- Write-through write to cache and DB together, synchronously. Cache always fresh; writes are slower.
- Write-behind (write-back) write to cache now, flush to DB asynchronously. Fast writes; risk of data loss if the cache dies before flushing.
- TTLs are your safety net. Per-key expiry bounds how stale any entry can get, regardless of pattern.
1. Why a cache needs a pattern at all
Redis is an in-memory data store sub-millisecond reads, a simple API, and rich data types (strings, hashes, sets, sorted sets, streams, even JSON and vectors). That speed is why it fronts databases for caching, sessions, leaderboards, rate limiting, and more. But the moment you keep a copy of data in the cache, you’ve created a consistency problem: the source of truth can change, and now your copy is wrong.
A caching pattern is the discipline that defines exactly how reads and writes move between the app, the cache, and the database and therefore how and when the two are reconciled. There’s no universally best pattern; there’s the right one for your read/write mix and your tolerance for staleness. The rest of this article walks the four that cover almost every real system, split by what they optimize: the read patterns (how data gets into the cache) and the write patterns (how changes get out to the database).
2. Read patterns: getting data into the cache
---
config:
theme: dark
fontSize: 17
themeVariables:
fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart TB
subgraph CA["🔄 Cache-aside (app manages it)"]
direction LR
A1["App"]:::obs -->|"1. check"| C1[("Cache")]:::gate
C1 -->|"miss"| A1
A1 -->|"2. read"| D1[("DB")]:::server
A1 -->|"3. populate"| C1
end
subgraph RT["📥 Read-through (cache manages it)"]
direction LR
A2["App"]:::obs -->|"read"| C2[("Cache")]:::gate
C2 -->|"miss → load"| D2[("DB")]:::server
end
classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
classDef gate fill:#D7C3F2,stroke:#8E5BD0,stroke-width:2px,color:#0F172A
classDef server fill:#A8E6D0,stroke:#2FA37C,stroke-width:2px,color:#0F172A
Figure 2: Two read patterns cache-aside puts the app in charge of misses; read-through pushes that job into the cache.
Cache-aside (lazy loading) the default. The application owns the logic: check the cache; on a hit, return it; on a miss, read from the database, populate the cache, and return. Its virtues are why it’s the most common pattern it’s simple, and it’s resilient: if the cache is down, the app just talks to the database directly. The trade-offs: every key’s first read is a miss (a “cold cache” penalty), and because the app writes the cache separately, you must handle invalidation when data changes (often via TTL).
Read-through. Same read flow, but the cache itself loads from the database on a miss, so the application only ever talks to the cache. This keeps app code clean and centralizes the loading logic at the cost of giving the cache (or a library/provider) more responsibility and coupling it to your data source.
Read replica / prefetch. For predictable hot data, you can warm the cache ahead of demand prefetching the entries you know will be requested (the home feed’s top items, a launch’s product list) so the first user doesn’t eat the miss. It trades a little wasted work for eliminating cold-start latency on the data that matters most.
The read decision usually comes down to where you want the complexity: cache-aside keeps it in your app (flexible, resilient); read-through hides it behind the cache (clean, coupled).
3. Write patterns: getting changes out to the DB
---
config:
theme: dark
fontSize: 17
themeVariables:
fontFamily: "Comic Sans MS, Comic Neue, Chalkboard SE, cursive"
---
flowchart TB
subgraph WT["✍️ Write-through (synchronous)"]
direction LR
A1["App"]:::obs -->|"write"| C1[("Cache")]:::gate
C1 -->|"same op, sync"| D1[("DB")]:::server
end
subgraph WB["⏳ Write-behind (async)"]
direction LR
A2["App"]:::obs -->|"write"| C2[("Cache")]:::gate
C2 -.->|"flush later, async"| D2[("DB")]:::server
end
classDef obs fill:#AED6F1,stroke:#2E86C1,stroke-width:2px,color:#0F172A
classDef gate fill:#D7C3F2,stroke:#8E5BD0,stroke-width:2px,color:#0F172A
classDef server fill:#A8E6D0,stroke:#2FA37C,stroke-width:2px,color:#0F172A
Figure 3: Two write patterns write-through keeps the DB in lockstep (slower, safe); write-behind defers the DB write (faster, riskier).
Write-through. Every write goes to the cache and the database synchronously, as one operation. The cache is always fresh and consistent with the DB the strongest consistency of the write patterns. The cost is latency: every write pays for both stores, so it’s slower. Great when reads dominate and you can’t tolerate stale data.
Write-behind (write-back). Write to the cache immediately and flush to the database asynchronously (batched, in the background). Writes are fast because the app doesn’t wait on the DB excellent for write-heavy or bursty workloads. The danger is real: if the cache fails before flushing, you lose the un-flushed writes. Use it only where some loss/lag is acceptable, or with durability safeguards.
TTL is the safety net across all of them. Redis supports per-key time-to-live, so even if your invalidation logic has a gap, an entry can only be stale for so long before it expires and gets reloaded. A sensible TTL bounds the blast radius of any caching bug set one even when you also invalidate explicitly.
Why it matters: “just add a cache” is where staleness and lost-write bugs are born, because the hard part isn’t Redis it’s the contract between cache and source of truth. Reach for cache-aside by default (simple, resilient), read-through to clean up app code, write-through when freshness is non-negotiable, and write-behind only when speed outranks the risk of loss. Then put a TTL on everything as insurance. Choose the pattern deliberately and your cache is a speedup; choose by accident and it’s a source of mysterious bugs.
References
- Cache-Aside pattern (Azure Architecture Center) the canonical pattern writeup
- Caching strategies (AWS) lazy loading vs write-through, with trade-offs
- Redis data types beyond strings: hashes, sorted sets, streams, and more