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_cache (table.ex) +
EchoCache.Coherence.newer?/2 (coherence.ex) — the engine is Valkey.
The guarantee, stated plainly
write-through.md.txt · AdvantagesWrite-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 (EchoCache.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.
Classify a read after a write
write-through.md.txt · AdvantagesTake 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.
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.
The guarantee on EchoCache
table.ex · coherence.ex · verbatimTake an instrument reference row that was updated. EchoCache.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_cache — a write-through put, then a fetch. EchoCache.Table.put(:instruments, "AST0NgWEfAEJfs", new_value) # handle_call sets L2 (SET … PX) then L1 (insert) under one TTL -> {:reply, :ok} EchoCache.Table.fetch(:instruments, "AST0NgWEfAEJfs") # {: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 framed version, compared
The value carries a mint-time version; a coherence message drops a row only when its version is newer
— EchoCache.Coherence.newer?/2, quoted from the as-built coherence module:
# 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
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.
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.
Recap — no stale window
the order of operationsBecause 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.
References
sources · relatedSources
- Valkey — SET — the cache write that precedes the return, the reason the read is fresh; the
PXexpiry 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
- R1.02 · Write-throughthe module hub
- R1.02.1 · The synchronous dual writethe previous dive
- R1.02.3 · The latency costthe next dive
- R1 · Cachingthe chapter
- /echomqthe bus behind the coherence lane that carries the framed version