jonnify·redis /redis-patterns/caching/write-through/consistency

R1.02.2 · Write-through — dive 2

The consistency guarantee.

A read that follows a write-through write cannot be stale — because both layers were written on the same path, under one TTL, framed with the write’s mint-time version.

Grounding: the version <> value frame in echo/apps/echo_store (table.ex) + EchoStore.Coherence.newer?/2 (coherence.ex) — the engine is Valkey.

Move the read along the timeline. Select where the read happens to see which strategy stays fresh.
the write during after later write-through fresh cache-aside stale window
readout · Select a read position. After the write returns, write-through is fresh by construction; cache-aside has a window.
§1

The guarantee, stated plainly

write-through.md.txt · Advantages

Write-through guarantees read-after-write consistency for the cache: any read that begins after a write has returned serves the value that write produced, not an older one. The reason is structural, not probabilistic. The write does not return until the cache has been written, so by the time a later read runs, the cache already holds the new value. There is no interval in which the cache lags the source after the write completes.

In the as-built code the new value carries its own provenance: the put clause stores version <> value — a 14-byte branded mint-time version in front of the bytes. The version is not for the read after this write (that read is fresh because both layers were set in the one call); it is for the next writer’s late message. A coherence message compares versions and drops a row only when the incoming version is newer (EchoStore.Coherence.newer?/2), so a late stale invalidation can never erase a newer row. Freshness on the write path; newer-wins across writers.

Contrast cache-aside, which invalidates the cache on a write rather than updating it. After a cache-aside write, the cache is empty (or, if invalidation is deferred, briefly stale); the next read takes a miss, re-reads the source, and backfills. That works, but it opens a window: between the write and the backfill, a concurrent reader is served an empty or old cache. Write-through removes the window by paying for the cache write up front, on the write path.

read-after-write
A read that starts after a write has returned. Write-through guarantees it returns the written value.
stale window
An interval where the cache lags the source. Write-through has none after the write returns; cache-aside has one until the next backfill.
structural
The guarantee follows from the order of operations, not from timing or luck: the cache write precedes the return.
framed version
version <> value, the write’s mint-time branded prefix, so coherence across writers can compare newer-wins.

Notes on ValkeyThe L2 row written with SET … PX carries its expiry on the server’s own clock; the value is the only thing the read returns, so a single-key read is the whole before or the whole after, never a torn mix — valkey.io/commands/set.

§2

Classify a read after a write

write-through.md.txt · Advantages

Take a single key with an old value and a new value. A write-through write replaces both layers; a later read of that key returns the new value, every time. Pick a read scenario to see exactly what the cache returns and why — and where cache-aside would differ.

Classify the read. Select a scenario to read what the cache returns and why.
readout · Select a scenario. After the write returns the read is fresh; an expired key takes a miss and re-reads the still-current source.

Static verdicts — after the write → fresh (both layers set first) · during the write → the before or the after, never torn · after the TTL → a miss, then a re-read of the still-current source.

After the write returns, the read is fresh by construction. During the write, a read returns a consistent before-or-after value; only an expired key takes a miss and re-reads the still-current source.

§3

The guarantee on EchoStore

table.ex · coherence.ex · verbatim

Take an emoji-set row that was updated. EchoStore.Table.put sets the L2 Valkey row and the L1 ETS table on the same call; the write returns once both are done. A read of the same id is served from the cache and matches what was written — there is no moment after the write where the cache could hand back the old value.

# echo/apps/echo_store — a write-through put, then a fetch.
EchoStore.Table.put(:cm_emojisets, "EMS0ODMggk1d5N", new_value)
#   handle_call sets L2 (SET … PX) then L1 (insert) under one TTL  ->  {:reply, :ok}

EchoStore.Table.fetch(:cm_emojisets, "EMS0ODMggk1d5N")
#   {:ok, new_value, :hit}   — the L1 row holds the value the put produced

Across writers, the value’s framed version is what keeps order. The coherence comparison reads the 11-byte snowflake payloads — lexicographic equals chronological — so a stale late message loses to a newer row:

the application · newer-wins across writers

The framed version, compared

The value carries a mint-time version; a coherence message drops a row only when its version is newer — EchoStore.Coherence.newer?/2, quoted from the as-built coherence module:

echo/apps/echo_store/lib/echo_store/coherence.ex · newer?/2 · verbatimas-built
# Mint-order comparison across kinds: true when `a` was minted after `b`.
def newer?(<<_::binary-3, pa::binary-11>>,
           <<_::binary-3, pb::binary-11>>), do: pa > pb
#   compares the 11-byte snowflake payloads — lexicographic equals chronological
the guarantee

Write the cache on the write path, before the write returns; then a read after the write cannot return a value older than that write — read-after-write freshness with no stale window.

its EchoStore application

A fetch after a put returns the value the put produced; both layers were set under one TTL, and the framed version lets Coherence.newer?/2 keep order newer-wins across writers.

The consistency guarantee is the whole reason to pay for write-through: a read after a write is fresh, by the order of operations, not by chance.

§4

Recap — no stale window

the order of operations

Because both layers are written on the same synchronous call, under one TTL, a read that begins after a write-through write returns serves the value that write produced. There is no interval where the cache lags the source — the property cache-aside trades away for a cheaper write. The next dive is about what that guarantee costs: every write pays for the L2 round-trip, and the failure modes when one layer fails.

§5

References

sources · related

Sources

  • Valkey — SET — the cache write that precedes the return, the reason the read is fresh; the PX expiry the one case a later read can miss.
  • Valkey — Topics — single-key value semantics and the engine the L2 layer runs on (Valkey).
  • Redis — GET — the single-key read that returns one whole value, never a torn one.
  • Redis — Documentation — the read and write commands and their consistency semantics on a single key.

Related in this course