Reading from a cache is the easy half. The hard half is what happens when the data changes. The restaurant raises the price of a biryani. A dish sells out. A restaurant closes for the night. Now there are two copies of the truth, the database and the cache, and they disagree.
There are a few standard ways to handle writes, each with a name you'll hear in design discussions: invalidate on write, write-through, write-back and write-around. They differ in one question: when a write happens, what do you do with the cache, and in what order? In this lesson we'll go through each, see the races that bite, and pick one for each kind of Tiffin data.
Option 1: Update the database, delete the cache
We met this in the last lesson. It's the partner of cache-aside, and the default for most systems.
%% caption: Invalidate on write. The next read rebuilds the cache from the truth. sequenceDiagram autonumber participant R as Restaurant participant A as App server participant D as Database participant C as Cache participant K as Next customer R->>A: update a price A->>D: write the new price A->>C: delete the menu key K->>A: open the menu A->>C: get the menu C-->>A: miss A->>D: load the fresh menu A->>C: store it A-->>K: fresh menu
The write path doesn't put the new value in the cache. It just removes the old one, and lets the next read fetch the truth. That sounds lazy. It's actually deliberate, and to see why, look at the alternatives that go wrong.
Why delete, and not update the cache?
It's tempting to write the new menu straight into the cache, so the next reader gets a hit. But consider two writes close together. The restaurant owner changes the biryani to ₹300, and a second later their manager, on another tablet, changes it to ₹320.
| Time | Request A (₹300) | Request B (₹320) | Database | Cache |
|---|---|---|---|---|
| 1 | Writes ₹300 to database | ₹300 | old | |
| 2 | (pauses briefly, a slow network or a busy CPU) | Writes ₹320 to database | ₹320 | old |
| 3 | Writes ₹320 menu to cache | ₹320 | ₹320 | |
| 4 | Writes ₹300 menu to cache | ₹320 | ₹300 |
The database says ₹320. The cache says ₹300, and will keep saying it until the TTL runs out. Every customer sees the wrong price. Nothing crashed; two requests simply finished in a different order than they started, which happens all the time on a busy server.
If both requests delete instead, order doesn't matter. Deleting twice is the same as deleting once. The next reader loads ₹320 from the database.
Why the database first, and not the cache first?
The other tempting order is: delete the cache, then update the database. That opens a different gap:
- Writer deletes
menu:42. - A reader arrives, misses, reads the old menu from the database (the update hasn't happened yet).
- The reader stores the old menu in the cache.
- Writer updates the database.
The cache now holds the old menu, with a fresh TTL. Updating the database first closes most of this gap: by the time the cache is deleted, any reader that misses will find the new data.
On a write: change the database first, then delete the cache key. Let the next read refill it.
There's still one rare race left even in this order, which we'll look at in the next lesson, along with why the TTL is the safety net for it.
Option 2: Write-through
With write-through, every write goes to the cache and the database together, before the request returns. The cache is always updated with the new value, not just emptied.
%% caption: Write-through. The write isn't done until both copies are updated. sequenceDiagram autonumber participant W as Write request participant L as Cache layer participant D as Database participant C as Cache W->>L: new price L->>D: write L->>C: store the new value L-->>W: done, both copies updated
Usually a single layer (a library or a caching service) handles both, so every write goes through one path. Done that way, it has real benefits:
- Reads right after a write are hits. No miss, no burst of database load after every change.
- The cache is never empty for data that's been written. Good for data that's read heavily and immediately after it changes.
And costs:
- Writes are slower. Each one does two writes instead of one.
- You cache things nobody reads. Every write lands in the cache, whether or not anyone will ask for it.
- The two writes aren't atomic. If the database write succeeds and the cache write fails (or the other way round), they disagree. You need a rule for which wins, usually "database first, and on any cache failure, delete the key".
- The race from the table above comes back. Two concurrent writes can still update the cache in the wrong order, even through the same code. Write-through is only safe when writes to a key can't overlap, for example because they're queued one at a time, or because each cached value carries a version number and an older version is never allowed to overwrite a newer one.
A Tiffin example where write-through fits: a restaurant's open/closed flag. When a big restaurant opens at 6pm, thousands of customers start reading it within seconds. Writing the new value straight into the cache avoids a burst of misses at exactly that moment.
Option 3: Write-back (write-behind)
Write-back turns things around. The write goes only to the cache, the request returns immediately, and the cache (or a background job) writes changes to the database later, often in batches.
%% caption: Write-back. The database catches up later. sequenceDiagram autonumber participant W as Write request participant C as Cache participant F as Background flusher participant D as Database W->>C: new value C-->>W: stored, reply at once C->>F: changes, every few seconds F->>D: batch write
Benefits:
- Very fast writes. A Redis write takes half a millisecond.
- The database sees far fewer writes. A thousand updates to the same key in a minute become one database write.
- Bursts are absorbed. A sudden spike lands on the cache, and the database gets a smooth stream.
The cost is serious: for a while, the only copy of the data is in the cache. If the cache crashes before flushing, those writes are gone. And until the flush happens, anything reading the database directly (reports, other services, support tools) sees old data.
So write-back fits data where losing the last few seconds is acceptable, and where writes are frequent:
- A rider's latest location, updated every few seconds. Keep it in Redis; save a sample to the database every minute. If Redis restarts, the next update arrives within 10 seconds anyway.
- Counters like "menu views per restaurant" for analytics. Add up in Redis, flush every minute. Losing a minute of view counts doesn't matter.
And it never fits orders, payments, or anything else you promised the customer you'd saved. "Your order is placed" must mean the database has it.
Option 4: Write-around
Write-around is the simplest of all: write to the database, and don't touch the cache. Any cached copy stays until its TTL expires.
This is only acceptable when the data is rarely in the cache in the first place, or when being stale for one TTL is fine. For example, writing old orders into an archive, or editing a restaurant's long description, which is cached with a 1-hour TTL and nobody minds if it updates an hour later.
Choosing, side by side
| Strategy | Write goes to | Cache after write | Risk | Good for |
|---|---|---|---|---|
| Invalidate on write | Database, then delete key | Empty, refilled on next read | Rare stale race, bounded by TTL | Most data: menus, restaurant info |
| Write-through | Cache and database together | Fresh | Slower writes, caches unread data | Read-heavy data read right after it changes |
| Write-back | Cache now, database later | Fresh | Data loss if the cache dies | High-frequency, loss-tolerant data: locations, counters |
| Write-around | Database only | Stale until TTL | Stale reads for one TTL | Rarely read, or staleness truly fine |
For Tiffin:
| Data | Strategy |
|---|---|
| Menus and dish availability | Database, then delete key |
| Restaurant open/closed flag | Write-through |
| Rider's latest location | Write-back, sampled to the database |
| Menu view counts | Write-back, flushed every minute |
| Orders and payments | Not cached. Database only |
Most systems end up like this: one default (invalidate on write) for most data, and a couple of special cases where the access pattern clearly calls for something else.
Pick a strategy
For each, choose a write strategy and say what could go wrong with it: (a) a "likes" count on restaurant reviews, which can get hundreds of taps a minute on a viral review, (b) a customer changing their saved delivery address, (c) Tiffin's operations team changing the delivery fee in one area, (d) a kitchen marking a dish sold out.
Show one answer
(a) Write-back. Count in Redis, flush to the database every minute or so. Risk: if Redis loses data, a minute of likes vanishes. Nobody will notice.
(b) Database, then delete the key, if you cache addresses at all. Probably you don't: it's personal and read rarely. Just read it from the database.
(c) Write-around: database only, and let the short expiry do the rest. Fee rules are cached in each server's memory for a minute (from lesson 2), and there's no single key you can delete from every server's memory at once. So accept that the new fee takes up to a minute to apply everywhere, and make sure checkout calculates the fee on the server (so the price charged matches the fee in force when the order was placed).
(d) Database, then delete the menu key. It must disappear quickly. Risk: the rare race leaves the dish showing until the TTL. That's why checkout must also check availability in the database: the cache decides what customers see, never what they can buy.
What to remember
- When data changes, the default is: update the database, then delete the cache key. The next read refills it.
- Deleting is safer than updating the cache, because two concurrent writers can finish in the wrong order. Database first, then delete, closes most of the remaining gap.
- Write-through updates cache and database together. Good for data read heavily right after a change, at the cost of slower writes.
- Write-back writes to the cache and saves to the database later. Very fast, but data can be lost. Only for loss-tolerant data like locations and counters.
- Write-around leaves the cache alone and relies on the TTL. Fine only when staleness truly doesn't matter.
- Never use the cache as the only copy of anything you promised a customer.