Two clients edit the same cart concurrently — each based on v0. Last-Write-Wins keeps the greater timestamp and silently drops one write. A version number recognizes the two as concurrent and keeps both as siblings: v0 → v1{[milk]} → v2{[milk],[eggs]} → v3{[milk, eggs]}. Nothing is lost.
Two clients edit the same shopping cart at the same time. Each read the cart before the other's write existed, so neither saw the other — the two writes are concurrent. Last-Write-Wins resolves the clash by keeping the greater timestamp and throwing the other write away, silently: one customer's item just vanishes, no error. A version number per key does better. Because the server makes every client read-before-write and echo back the version it last saw, it can tell that both writes were "based on v0" and so neither supersedes the other — it keeps both as siblings and hands them to the app to merge. You will replay the book's cart example and watch the server go v0 → v1{[milk]} → v2{[milk],[eggs]} → v3{[milk, eggs]}, then run the identical two writes under LWW and watch milk disappear.
Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 6 — Replication, §"Detecting Concurrent Writes" / §"Capturing the happens-before relationship":
the server can return all siblings—all values that have not been overwritten.
The book develops the single-replica algorithm on the shopping-cart example. Here you implement that server and watch it detect the concurrency LWW erases.
The surprise is that a plain integer, not a clock, is what separates concurrent writes from sequential ones. These help; each line notes where it shows up.
write call in the transcript.lww_clock_skew.py in this chapter, and DDIA §"Last write wins". If only one is new, make it #1 — "concurrent" meaning causally unordered, not simultaneous, is the hinge of the whole exercise.
pip, no Docker. One key, cart, whose value is a list of items; two concurrent clients plus a later merging client, exactly as in the book's figure.
The server stores, per key, two things: a version number and a set of siblings — the values written since they were last overwritten, each tagged with the version at which it landed. The rules:
So a write "based on v0" arriving after another "based on v0" write does not overwrite it (that value sits at v1 > 0): both survive as siblings. Contrast this with LWW, which just keeps max(timestamp) and discards the rest. The whole exercise is one script, code/version_vectors.py.
cd study/ddia/ch06
python3 code/version_vectors.py
Do not read the output yet — make each prediction first.
Predict. The server starts empty at version 0. Client 1 reads v0, adds milk, writes based on v0. Client 2 also read v0 (before client 1's write existed), adds eggs, writes based on v0. Write down the version number and the sibling set after each write. Is [eggs] written on top of [milk] (replacing it), or alongside it?
Because client 2's write was based on v0, the server overwrites only values at version ≤ 0 — and [milk] sits at v1, so it stays. The result is a sibling set {[milk], [eggs]}, the server's way of saying "these two are concurrent; I cannot pick a winner, so here are both."
Predict. A later client reads at v2 and gets both siblings. It merges them to [milk, eggs] and writes based on v2. What happens to the two siblings now?
This write was based on v2 — the client had seen everything up to and including both siblings — so the server overwrites all values at version ≤ 2, which is both of them, and stores the merged cart at v3. The conflict is resolved by the app, and nothing was lost along the way.
Predict. Take the identical concurrent writes — client 1 adds milk, client 2 adds eggs — and resolve them with Last-Write-Wins (keep the greater timestamp). What does the cart contain afterward?
Same two writes, two outcomes: version numbers preserve [milk, eggs]; LWW keeps only [eggs] and drops milk with no error, no conflict, no trace.
v0 → v1{[milk]} → v2{[milk], [eggs]} → v3{[milk, eggs]}; the two "based on v0" writes become siblings, not one overwriting the other.[eggs]; milk is silently discarded.The difference is what each scheme uses to decide whether one write may replace another. LWW uses a timestamp and the rule "bigger wins." But a timestamp is just a number on a clock; it imposes a total order on writes that were never actually ordered. The two cart writes happened concurrently — neither client saw the other — yet LWW must still declare one "later," and the "earlier" one is deleted. The information that they were concurrent is simply gone by the time LWW compares timestamps.
The version number keeps that information. Because every client reads before it writes and echoes back the version it saw, the server can ask a sharper question than "which is newer?": it asks "which values had this writer already seen?" Those, and only those, its write is entitled to overwrite. Client 2 had seen only v0, so it may overwrite values at v0 — but [milk] was written at v1, after what client 2 saw, so client 2 has no authority over it. The server keeps both. This is precisely the happens-before relation: a write supersedes another only if it causally follows it (saw its version). When neither saw the other, the writes are concurrent and the server preserves both siblings — pushing the choice up to the application, which knows that two carts should be merged, not that one should win.
[milk] ∪ [eggs] = [milk, eggs]; a different app might need a different rule (and deletions need tombstones, or a merged-away item can resurface). A single integer works because there is one replica assigning versions. With multiple replicas each accepting writes, one counter is not enough: you need a version vector — one version number per replica — so that "which writes had this client seen?" can be answered per replica. That is the leaderless / multi-leader case, and the vector is the generalization of the single number you just watched work.
read return the pair. Work out the merge rule for two vectors (component-wise max) and confirm it still calls the milk/eggs writes concurrent — this is the leaderless case DDIA covers next.Sources: DDIA 2e, Ch. 6, §"Detecting Concurrent Writes", §"Capturing the happens-before relationship", §"Version vectors" · Lamport, Time, Clocks, and the Ordering of Events in a Distributed System (1978).