DEV Community

Muralidharan Lakshmanan
Muralidharan Lakshmanan

Posted on

Cache Patterns Every Engineer Should Know

In the previous parts of this series, we answered two questions: why do we need caching, and where should the cache live?

Now we need a more practical one:

How should the application actually interact with the cache?

This is where caching patterns come in. A cache is not just a box where we put data. We need a strategy for reading data, handling misses, writing data, updating cached values, and dealing with staleness. Different patterns solve these problems differently. Let's look at the most common ones.


1. Cache-Aside — the most common pattern

Let's start with Cache-Aside, also called lazy loading. The basic idea: the application is responsible for checking the cache and loading data on a miss.

GET /products/123

Application → Cache → HIT → Product. Done.

Application → Cache → MISS
                        ↓
                    Database
                        ↓
                  Store in cache
                        ↓
                    Response
Enter fullscreen mode Exit fullscreen mode
Product product = cache.get("product:123");

if (product == null) {
    product = database.findProduct(123);
    cache.put("product:123", product);
}

return product;
Enter fullscreen mode Exit fullscreen mode

The next request can be served from the cache.

Cache-Aside is popular because it's simple. The application explicitly decides what to cache, when to cache it, what TTL to use, what to do on a miss, and when to invalidate it. It also works with almost any cache technology.

The downside is that the application now carries caching logic. Developers need to remember the check-fallback-populate sequence, and if several services do this independently, that logic tends to get duplicated. Still, Cache-Aside is often the best starting point.


2. Read-Through — let the cache do the loading

Read-Through moves that responsibility away from the application. Instead of the application saying "if the cache misses, I'll query the database," it simply says "give me the data." The cache handles the miss itself.

Who handles the cache miss: in Cache-Aside the application falls back to the database directly; in Read-Through the cache queries the database on its own and the application only ever talks to the cache

The application code gets simpler:

product = cache.get("product:123");
Enter fullscreen mode Exit fullscreen mode

If the cache doesn't have it, the cache layer knows how to retrieve it. The application doesn't need to know as much about the underlying data source, which can produce cleaner application code.

The catch is that not every cache supports this natively — you need a caching layer or framework that knows how to load the missing data, which means more abstraction and configuration. Read-Through can be elegant, but Cache-Aside is usually easier to understand and implement directly.


3. Write-Through — write to the cache and database together

So far we've mostly talked about reads. But what happens when data changes? Suppose a customer updates their address. Now the database and the cache both need to reflect the new value.

With Write-Through, a write goes through the cache, and the cache updates the underlying data store as part of the same operation:

Update customer address
          ↓
       Cache
          ↓
      Database
Enter fullscreen mode Exit fullscreen mode

The main benefit is freshness — the cache is updated as part of the write path, so the system reduces the chance of serving an old value. The cost is that writes become more expensive: instead of Application → Database, a write now involves Application → Cache → Database. We're trading write performance and complexity for better cache freshness.


4. Write-Behind — make writes fast

Now the opposite approach. What if writes are extremely frequent — say, 50,000 updates per second? Writing every one immediately to the database may be expensive.

With Write-Behind, the cache accepts the update first and the write returns immediately. The database catches up later, asynchronously.

Write-Through vs. Write-Behind: Write-Through only returns once both the cache and database are updated, while Write-Behind returns as soon as the cache is updated and persists to the database later — risking data loss if the cache fails before that happens

This can make writes extremely fast. But there's a serious trade-off: what happens if the cache crashes before the update reaches the database? Potentially, data loss.

That's why Write-Behind shouldn't be treated as simply "a faster Write-Through." It's a fundamentally different consistency and durability model. It works best when eventual persistence is acceptable, the cache has reliable durability mechanisms of its own, updates can be replayed or recovered, and extreme write performance genuinely matters.


5. Refresh-Ahead — don't wait for the cache to expire

Here's another problem. Suppose product:123 is cached with a TTL of 10 minutes, and thousands of users are requesting it. At minute 9, every request is a hit. At minute 10, the cache expires — and thousands of requests can arrive at the database at the same instant.

We touched on this earlier in the series. It's commonly called a cache stampede or thundering herd.

Refresh-Ahead tries to avoid it. Instead of waiting for the value to expire, the system refreshes it shortly beforehand, while the existing cached value keeps serving requests in the meantime.

Refresh-Ahead avoids the thundering herd: without it, every request piles onto the database the instant the TTL expires; with it, a background refresh updates the value before expiry so every request stays a hit

Imagine a flight-search application where a popular route is requested thousands of times. Instead of letting the cache expire completely, the system refreshes the data once it has, say, 30 seconds left. The next request doesn't have to wait for a database call — it just gets served from an already-fresh cache.


6. Comparing the patterns

Pattern Who handles the miss? How writes work Main benefit
Cache-Aside Application Application manages writes Simple and flexible
Read-Through Cache Depends on implementation Cleaner application code
Write-Through Cache layer Cache and database together Better freshness
Write-Behind Cache Database updated later Very fast writes
Refresh-Ahead Cache / background process Usually a normal write strategy Reduces cold misses

There is no universally "best" pattern. The workload determines the answer.


7. A real-world example

Let's imagine an e-commerce product with a name, description, price, inventory count, and reviews. Should all of it use the same caching pattern? Probably not.

One product, four caching strategies: the description uses plain Cache-Aside with a long TTL, price adds explicit invalidation, inventory uses a short TTL or bypasses the cache, and recommendations combine Cache-Aside with Refresh-Ahead

The description changes rarely, so plain Cache-Aside with a one-hour TTL is fine. The price changes more often, so we pair Cache-Aside with explicit invalidation the moment it changes. Inventory is more sensitive — a stale number could let a customer buy something that's no longer available, so a much shorter TTL, or skipping the cache in some parts of the workflow, makes more sense. Recommendations can be expensive to calculate, which makes them a good candidate for Cache-Aside plus Refresh-Ahead.

That's an important lesson: don't choose one caching pattern for your entire system. Choose the pattern based on the behavior of the data.


8. The hidden complexity: cache invalidation

Suppose the database has product:123 at $899, but the cache still has $999. A caching pattern doesn't automatically solve this — you still need to decide what happens to the cache when the database changes.

The common answers are to delete the entry and let the next request reload it, update the cached value directly, let the TTL expire naturally, or invalidate the cache in response to a published event when the database changes.

This is a big enough topic that the next part of this series is devoted entirely to it.


9. Which pattern should you start with?

If you're designing a new application and aren't sure what to use, don't reach for the most sophisticated pattern first. For many read-heavy applications, plain Cache-Aside in front of the database is an excellent starting point.

Then measure. Is the hit ratio good? Is database load actually reduced? Are misses expensive? Are hot keys causing problems? Is stale data acceptable? Are writes becoming a bottleneck? Only once you have answers should you introduce something more sophisticated.


10. The engineer's mental model

Here's a short way to hold all five patterns in your head at once:

  • Cache-Aside — "I'll manage the cache myself."
  • Read-Through — "The cache will load missing data for me."
  • Write-Through — "When I write, update the cache and the source together."
  • Write-Behind — "I'll make the cache the fast write point and persist later."
  • Refresh-Ahead — "Don't let popular data go cold if I can refresh it proactively."

Once these five ideas are second nature, most caching architectures become much easier to reason about.


What's next

We've now covered why caching exists, how it actually works, where it should live, and the patterns that govern how an application talks to it.

But there is one caching problem that almost every engineer eventually runs into:

How do you make cached data disappear when the real data changes?

That's cache invalidation — often called the hardest problem in caching — and it's next.


Which pattern is running in your production system right now, and did your team choose it deliberately or inherit it? Curious to hear in the comments.

Top comments (0)