So, what is a cache?
A cache is a small, fast place where you keep copies of things you’ll probably need again soon.
Think about your kitchen. Your rice, spices and oil live in the pantry, but the salt you use every day sits right next to the stove. You didn’t move it there because the pantry is broken. You moved it because walking to the pantry ten times a day gets old fast. The stove-side shelf is your cache.
Software works the same way. Your database is the pantry: it holds everything, it’s reliable, and it’s slow-ish to reach. A cache (usually something in memory, like Redis or Memcached) is the shelf next to the stove. It holds a small slice of your data, and it hands it back in a fraction of the time.
Two words you’ll see a lot below:
- Cache hit: the data you asked for is already in the cache. Lovely, you’re done.
- Cache miss: it isn’t there, so someone has to go fetch it from the database.
Why bother with a cache at all?
Because most apps read the same data over and over, and asking the database every single time is wasteful.
Open any popular app and think about what it fetches: your profile, a product page, today’s trending posts, a settings object. Thousands of people ask for the exact same thing within seconds of each other. A cache lets you answer all of them without bothering the database each time.
Here’s what that buys you:
- Speed. Reading from memory is far quicker than a database query that might touch disk, join tables or cross the network. Pages feel snappy.
- Less load on the database. Every request the cache answers is one the database never sees. That means it stays healthy during traffic spikes instead of falling over.
- Lower cost. A busy database is an expensive database. Offloading repeat reads often means you can run a smaller one.
- Room to breathe. If the database is slow or briefly unavailable, a cache can keep serving the popular stuff for a while.
There’s a catch, though, and it’s the whole reason this post exists. The moment you keep a copy of data in two places, those copies can drift apart. The cache might say a product costs $20 while the database already says $25. That’s called stale data, and every caching strategy is really just a different answer to two questions: who loads data into the cache, and what happens when data is written?
The first group of strategies answers the reading half. The second answers the writing half.
Read strategy 1: Cache-aside (a.k.a. lazy loading)
Cache-aside is the one you’ll run into most, and the idea is refreshingly simple: the cache just sits there, and your app does all the legwork.
When a request comes in, the app checks the cache first. If the data is there, great, send it back. If not, the app goes to the database itself, grabs what it needs, and drops a copy into the cache on the way out so the next visitor gets the fast path.
On a hit, step 2 returns the data and the request is done. Notice that the cache never talks to the database. The app is the middleman for everything.
Why people like it. Only data that someone actually asked for ends up in the cache, so you aren’t burning memory on stuff nobody reads. And if the cache crashes, nothing breaks. The app just gets slower for a while because every request falls through to the database.
What to watch out for. The first request for anything is always a miss, so it’s slow. And since the cache has no idea when the database changes, it can happily serve an old value. Most teams handle this by putting an expiry time (a TTL) on each key, or by deleting the key whenever the record is updated.
Good fit for: read-heavy apps where a value being a few seconds out of date isn’t a disaster. Product pages, user profiles, blog posts like this one.
Read strategy 2: Read-through
Read-through looks a lot like cache-aside from far away, but there’s one big difference: the app stops talking to the database for reads. It only ever asks the cache.
If the cache has the data, it returns it. If it doesn’t, the cache itself goes to the database, loads the value, keeps a copy, and then hands it back. From the app’s point of view, the cache simply always has the answer.
Compare this with the cache-aside diagram above. There, the arrow to the database started at the app. Here it starts at the cache. The database is the cache’s problem, not the app’s.
Why people like it. Your application code gets cleaner. There’s no “check cache, else query DB, then save to cache” logic sprinkled across every endpoint. You configure the loading once, in the caching layer (or a library that wraps it), and forget about it.
What to watch out for. You need a cache that knows how to talk to your database, which usually means a library, a provider, or a managed service that supports it. The first-request-is-slow problem is still there, and so is stale data, so you still need TTLs or some way to invalidate keys.
Good fit for: the same read-heavy workloads as cache-aside, when you’d rather keep that loading logic out of your app.
Write strategy 1: Write-through
Now for the other half of the problem: what happens when data changes?
With write-through, every write goes to the cache first, and the cache immediately writes it to the database too. The app doesn’t get a “done” until both copies are saved. It’s the belt-and-braces option.
The shaded area is the important bit. The app is stuck waiting for the whole round trip, cache and database, on every single write.
Why people like it. The cache and the database always agree. You never read a stale value right after updating it, and if the cache crashes, nothing is lost because the database already has everything. It also pairs nicely with read-through: reads and writes both go through the cache, so the cache is always warm with fresh data.
What to watch out for. Writes are slower, because you’re paying for two saves instead of one. And you may end up caching lots of data that gets written once and never read again, which is wasted memory.
Good fit for: data that’s read soon after it’s written and where being wrong is not okay. Think account balances, user settings, or inventory counts.
Write strategy 2: Write-around
Write-around takes the opposite view: writes don’t touch the cache at all. They go straight to the database, and that’s it.
The cache only gets the new data later, if and when somebody reads it. At that point you’re back in cache-aside territory: miss, read from the database, save a copy.
The top box is the write, quick and simple. The bottom box is what happens the first time anyone asks for that data afterwards. The cache only fills up once someone actually reads the data.
Why people like it. The cache doesn’t get flooded with data nobody reads. If you’re importing millions of log lines or writing records that are rarely looked at again, there’s no point shoving them into precious memory.
What to watch out for. The first read after a write is always a miss, so it’s slow. There’s also a sneaky stale-data trap: if an older copy of that key is already sitting in the cache, the write to the database won’t replace it. In practice, most teams delete (invalidate) the cached key whenever they write, so the next read is forced to fetch the fresh version.
Good fit for: write-heavy data that’s read rarely, or not right away. Logs, chat history, audit trails, bulk imports.
Write strategy 3: Write-back (a.k.a. write-behind)
Write-back is the speed demon of the group. The app writes to the cache, gets an instant “done”, and moves on. The database finds out later.
Every so often (on a timer, or once enough changes have piled up) the cache flushes its pending writes to the database in one batch.
Notice that the database doesn’t appear at all in the top box. The app gets its answers purely from the cache. If the cache dies before step 5, values A and B never reach the database.
Why people like it. Writes are about as fast as they can be, since you’re only touching memory. And batching is great for the database: ten updates to the same counter can collapse into a single write, which takes a lot of pressure off during busy periods.
What to watch out for. This is the risky one. Until the flush happens, the only copy of your newest data lives in the cache. If the cache crashes or restarts in that window, those writes are simply gone. It’s also harder to build and debug, because the cache and database are briefly out of sync by design.
Good fit for: high-volume writes where losing a few seconds of data wouldn’t hurt much. View counters, likes, analytics events, game scores, sensor readings. Probably not payments.
So which one should you pick?
Honestly, you’ll usually pick one read strategy and one write strategy, and use them together. Here’s the whole thing on one screen:
| Strategy | Type | Who talks to the DB | Biggest win | Biggest risk |
|---|---|---|---|---|
| Cache-aside | Read | The app | Simple, survives cache crashes | Stale data, slow first read |
| Read-through | Read | The cache | Cleaner app code | Needs a cache that can load from your DB |
| Write-through | Write | The cache, right away | Cache and DB always agree | Slower writes |
| Write-around | Write | The app, skipping the cache | Cache isn’t cluttered with cold data | First read after a write is a miss |
| Write-back | Write | The cache, in batches | Very fast writes | Data loss if the cache dies before flushing |
A few combos that show up again and again in real systems:
- Cache-aside + write-around (deleting the cached key on write). The default for most web apps. Easy to reason about, hard to break badly.
- Read-through + write-through. The cache becomes the front door for everything, and stays fresh. Great when consistency matters.
- Read-through + write-back. Maximum speed for write-heavy workloads, as long as you can live with a small risk of losing recent writes.
If you’re not sure, start with cache-aside and a sensible TTL. It’s forgiving, it’s easy to debug, and you can always get fancier once you actually see where your app is slow.
The main thing to remember is that a cache is a copy, and copies go out of date. Every strategy here is just a different trade between speed, freshness and safety. Once you know which of those your feature cares about most, the choice mostly makes itself.