Skip to main content
How Caching Workslesson 5 of 6 · 2 min read

Writing Through a Cache

Writes are the hard half

Reads were the easy half. The moment somebody edits a price you have two copies that disagree, and you have to choose what to do about it.

The simplest thing that works: write to the database, then delete the key from the cache. Delete it, do not update it. Deleting means the next reader misses and rebuilds from the database, which costs one extra read on something that was about to be read anyway.

Updating is worse, because it looks tidier and it is not. Updating means writing the new value in two places and hoping both land in the same order. If two edits arrive at once and reach your database in one order and your cache in the other, the cache keeps the loser and serves it until the time limit runs out. A delete cannot get the order wrong, because there is no value left to be stale.

One ordering is always wrong: writing to the cache before the database. A crash between those two lines leaves your cache serving a price that was never saved anywhere. That is the one arrangement that can invent data. Database first, then delete.

A gap still remains even when you do it correctly. Between the database commit and the delete, readers see the old value. That window is usually milliseconds, and for a price or a profile picture it does not matter. When it does matter, the answer is not a cleverer cache. The answer is not caching that particular thing.

the shape of it
Your serverDatabase1. write hereRedis2. delete keyNext readermisses, rebuildssavedeleteone miss
step 1 of 2
Database first, then delete. One reader pays for a miss and the copy is correct again.
the write path, and the one to avoid
Java
void updatePrice(long id, BigDecimal price) {
  // 1. The truth goes first. If this fails you have promised
  //    nothing to anyone.
  db.execute("UPDATE products SET price = ? WHERE id = ?", price, id);

  // 2. Delete, do not update. There is no value left to be stale.
  redis.del("product:" + id);
}

// Never this way round. A crash between the two lines leaves
// the cache serving a price that was never saved:
//   redis.setex(key, 300, newJson);
//   db.execute("UPDATE ...");        <-- never runs

Worked example

Arjun's first version updated the cache instead of deleting it, and it held up for a month. Then a bulk price import ran two workers over the same product. The two updates reached his database in one order and Redis in the other.

His database ended up with the correct price of 24,999. Redis held 27,499 and served it to every customer for the next five minutes.

Switching to a delete removed that whole class of bug. It cost one extra database read per price change, on a system doing 400 price changes an hour against 3,000 reads a second.