Cache-aside and the read and write paths
The database is the truth; the cache is whatever was written to it last. Delete after the commit, and the next read restores the truth.
Predict first
Before you read: predict
ExamHall serves GET /exams/:id/config with cache-aside on one shared Redis (TTL 1 h). On an edit, the handler updates Postgres, then SETs the new config in Redis. ECON-201 ends at 10:30 and is cached. Two admins edit it within a second: admin A's request commits 10:45, then admin B's commits 11:00. B's handler runs its SET first; A's SET lands after it.
What do candidates read from then on?
The 30-second version
Cache-aside reads the cache and, on a miss, loads the row from the database and stores it. The cache keeps whatever was written to it last, not what the database committed last. So on a write, commit, then delete the key: deletes in any order let the next read load the committed row. Setting values can land out of order.
The database is the truth; the cache is whatever was written to it last. Delete after the commit, and the next read restores the truth.
How it works
Step 1 of 11
In the kitchen
- a gap on the shelf: print from the price file, clip the label on → a miss: load the row, SET it (step 1)
Analogy: A supermarket's shelf-edge price labels
Head office keeps the price file. Each shelf carries one label per product. An assistant who finds a gap prints a label from the price file and clips it on.
| In the supermarket | In the system |
|---|---|
| a gap on the shelf: print from the price file, clip the label on | a miss: load the row, SET it (step 1) |
| the price file | the database, the truth (step 2) |
| two price changes; two assistants carry new labels and arrive in the wrong order | crossed SETs (step 3) |
| change the file, then pull the label; the next assistant prints the current price | delete after the commit (step 5) |
| pull the label, then change the file; someone reprints the old price in between | delete before the commit (step 6) |
| the till takes the new price now; head office hears at the evening upload | write-behind (step 10) |
In real terms: the cache is a copy anyone may overwrite; deleting after the commit hands the next reader the truth, and the TTL caps what slips through.
Breaks down when: a shopper sees a label that disagrees with the till and complains, so a wrong label gets noticed. Wrong inference: "a stale entry gets noticed and fixed soon." A cache never compares its copy with the database; only a delete, a write or the TTL replaces it.
Contrasting case: no labels; every shopper asks at the till. Nobody sees a wrong price, and everyone queues. That is no cache: every read goes to the database.
Examples, run for real
4.1 ExamHall: two edits, and the first one won
EXH-733: two admins changed ECON-201's duration within a second, and for the next hour candidates got the first edit. The handler SET the config after each commit; MR !1204 deletes the key instead. Same forced interleaving:
4.1 two admins edit ECON-201 duration: A commits 105, B commits 120; B's cache step runs, then A's
before (SET after commit) database 120 · cache 105 · next 3 reads 105 105 105 · database loads 0
after MR (DEL after commit) database 120 · cache (empty) · next 3 reads 120 120 120 · database loads 1
Before: the cache kept the last SET, which carried the older commit (steps 2–3). After: two deletes in either order leave no key, and one miss loads the committed row (step 5).
4.2 Ticketing: when the delete runs
An event moves from 19:30 to 20:00 while a fan's read misses; gates force each order.
4.2 an event moves from 19:30 to 20:00 while a fan's read misses (TTL 60 s)
DEL, then UPDATE; the read runs between them fan stored 19:30 · database 20:00 · next reads 19:30 19:30 · stored copy expires within 60 s
UPDATE, then DEL; the read runs after the DEL fan stored 20:00 · database 20:00 · next reads 20:00 20:00 · stored copy expires within 60 s
UPDATE, then DEL; the read loaded before the UPDATE and stored after the DEL fan stored 19:30 · database 20:00 · next reads 19:30 19:30 · stored copy expires within 60 s
Deleting first left a gap the read filled with the old row (step 6). Deleting after the commit fixed it; the third line is the race that remains, capped by the TTL (steps 7, 11).
4.3 Where the write lands
Five answer saves through the pack's cache layer; the write-behind run ends by killing Redis.
4.3 a candidate saves 5 answers through a cache layer
read-through: the app called layer.get 3 times · loader ran 1 · app code database calls 0
write-through: 5 saves acknowledged · in Postgres 5 · reads right after each save from the cache 5/5
write-behind: 5 saves acknowledged · flushed after save 3 · Redis killed (SIGKILL) · in Postgres 3 of 5 acknowledged
Read-through moved the loader out of the app (step 8). Write-through waited for Postgres and left the cache warm (step 9). Write-behind acknowledged two saves Postgres never got (step 10).
Picture it

Try it
Answer first, then check. Nothing is saved.
Question 1 of 5
A box office uses write-behind: each seat purchase is written to Redis and queued, and a flusher writes the queue to Postgres after every 50th queued purchase. 120 purchases are acknowledged, then the Redis node crashes and its memory is gone.
How many purchases are in Postgres?
Question 2 of 5
A price change runs in one transaction:
The insert hits a duplicate key and the transaction rolls back, yet shoppers see 19.99. Shoppers read the key all the time, including while the transaction is open.
Which change stops the cache showing a price the database rejected?
await db.transaction(async (tx) => { const [row] = (await tx.query(`UPDATE products SET price = 19.99 WHERE id = 'P9' RETURNING price`)).rows; await cache.set('product:P9', JSON.stringify(row), { expiration: { type: 'EX', value: 3600 } }); await tx.query(`INSERT INTO price_history VALUES ('P9', '2026-10-06', 19.99)`); // fails: that day's row exists});Question 3 of 5
A player reports each viewer's position every 2 s: 20 viewers, 600 updates a minute. Losing up to a minute of a position in a crash is acceptable. The database can take at most 40 writes a minute.
Which write path meets both limits?
Question 4 of 5
A repricing job moves all 20 price tiers of an event. Pages read tiers with cache-aside (TTL 1 h). The job:
Tier 8 was not cached when a run started, and pages read it during the run. Afterwards tier 8 keeps its old price for an hour; the other 19 are right.
What went wrong?
async function reprice(eventId, tiers) { for (const t of tiers) { await db.query('UPDATE tiers SET price = $2 WHERE id = $1', [t.id, t.price]); await cache.del(`tier:${eventId}:${t.id}`); }}Question 5 of 5
A chat app's profile service has three goals: the app code never queries the database for profiles; the read right after a profile update is a cache hit with the new name; and an update is acknowledged only once Postgres holds it.
Which design meets all three?
Go deeper
Pro tips
- Write the version with the value, and refuse older ones. Store
{v, version}and have the fill or the update SET only when its version is newer, in one atomic step on the server. When: update-on-write is required (for example, the cache must never miss after a write) and writers can cross (steps 3–4). Status: DOC. The memcache paper describes the same problem as a "stale set" and fixes it with leases, tokens the cache checks before it accepts a set. Not run here; the atomic compare belongs tonosql-redis-data-structures. This is a version on the value, checked at SET; versioned keys (a new key per version) belong tocache-invalidation-strategies. - Log the order of the three events per key when you debug staleness: the commit, the cache delete or SET, and every fill. When: a report says "the old value came back after the edit" (steps 4, 6, 7).
Source
- Microsoft Azure Architecture Center, Cache-Aside pattern: https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside · fetched 2026-09-27. Read path steps 1–3; on update the application "writes the change to the data store and then invalidates the corresponding item in the cache"; "Update the data store before removing the item from the cache", because otherwise a fetch in between retrieves "the outdated item" and adds it back. It also says the pattern "doesn't guarantee consistency" (step 7). Valid as of 2026-09-27.
- Microsoft Azure Architecture Center, Caching guidance, "Keep the cache consistent on writes": https://learn.microsoft.com/en-us/azure/architecture/best-practices/caching · fetched 2026-09-27. Invalidate on write: a reader "can experience a cache miss or briefly see stale data"; write-through: "return the write response only after both updates succeed", "at the cost of higher write latency" (steps 5, 9). Valid as of 2026-09-27.
- AWS whitepaper, Database Caching Strategies Using Redis, "Caching patterns": https://docs.aws.amazon.com/whitepapers/latest/database-caching-strategies-using-redis/caching-patterns.html · fetched 2026-09-27 (AWS marks it "for historical reference only"). Cache-aside (lazy loading) read path; its "write-through" is the application updating the database and "Immediately afterward" the cache, which is this lesson's update-on-write. Valid as of 2026-09-27.
- Oracle Coherence 14.1.2, Caching Data Sources: https://docs.oracle.com/en/middleware/fusion-middleware/coherence/14.1.2/develop-applications/caching-data-sources.html · fetched 2026-09-27. Read-through: on a miss the cache "delegates to the CacheStore"; write-through: the
putdoes not return until the data source has stored it; write-behind: entries are "asynchronously written to the data source after a configured delay", repeats are "coalesced", and write-behind "effectively makes the cache the system-of-record" until the queue is written (steps 8–10; the pack'scacheLayerfollows these semantics). Valid as of 2026-09-27.
Related lessons
Checked against the tools and docs as of 2026-09-27.