πŸ“NoteπŸ’»Tech

Vector Clocks and Inconsistency Resolution in Distributed Systems

Tony Duong

Tony Duong

Aug 2, 2026 ・ 7 min

#system-design#distributed-systems#vector-clocks#consistency#interview#dynamo
Vector Clocks and Inconsistency Resolution in Distributed Systems

In distributed systems, two writes can happen at the "same time" on different machines β€” and if you resolve them with ordinary timestamps, you will quietly throw one away. Vector clocks exist so you can tell happened-before from actually concurrent, and hand real conflicts to the application instead of guessing.

The problem: wall-clock time lies

The naive approach to concurrent writes:

  1. Each replica stamps a write with now()
  2. On conflict, keep the version with the later timestamp
  3. Drop the other one ("last write wins")

That fails in production for a boring reason: server clocks drift.

  • NTP sync is imperfect; machines can be milliseconds or seconds apart
  • A write that logically happened first can get a later timestamp
  • The system then keeps the wrong version and silently loses data

Example of silent loss:

  • Replica A (clock slightly ahead) saves cart: { eggs } at T=100
  • Replica B (clock slightly behind) saves cart: { bacon } at T=99
  • Merge rule: keep A β†’ bacon disappears
  • Nobody throws an error. The item is just gone.

You cannot safely order events across machines with wall-clock time alone. You need a way to track causality β€” which writes knew about which other writes.

The solution: vector clocks ("Git for data")

A vector clock does not ask "what time is it on the wall?" It asks: "what version of history has each node seen?"

Mental model: Git for data.

  • Each replica is like a branch contributor
  • Each write increments that node's counter
  • Comparing clocks is like comparing commit histories:
    • One history contains the other β†’ fast-forward
    • Histories branched β†’ merge conflict

Format

A vector clock is a map of NodeName β†’ VersionNumber:

[A: 1, B: 2, C: 1]

Rules of thumb:

  • On a write handled by node X, increment X's counter
  • When replicas sync, take the element-wise max of the two clocks
  • Omit nodes at version 0 for readability (same meaning)

This tracks causal history, not UTC.

A real-world scenario: the shopping cart

Classic Dynamo-style example. A user has a shopping cart. The network is spotty. Writes land on different replicas before they sync.

Replicas: A, B, C.

Step 1 β€” Create the cart (on A)

User creates an empty cart (or first item). Replica A handles the write.

Cart: { milk }
Clock: [A: 1]

Only A has written. Everyone else is at zero for this key.

Step 2 β€” Add eggs from the phone (on A)

Still talking to A. User adds eggs.

Cart: { milk, eggs }
Clock: [A: 2]

[A: 2] descends from [A: 1]. Safe overwrite β€” A already knew about the previous version.

Step 3 β€” Concurrent update: bacon from the laptop (on B)

The laptop hits replica B before B has seen A's latest cart. B only knows about an older (or empty) state β€” or receives a stale client context. For the interview story, treat B's write as concurrent with A's eggs update.

B adds bacon based on what it believes is current:

# What B writes (concurrent with [A: 2])
Cart: { milk, bacon }
Clock: [A: 1, B: 1]

Why [A: 1, B: 1]?

  • B knew about the cart after A's first write (A: 1)
  • B had not seen the eggs write (A: 2)
  • B increments its own counter β†’ B: 1

You now have two versions in the system:

Version Cart Vector clock
V2 (from A) { milk, eggs } [A: 2]
V3 (from B) { milk, bacon } [A: 1, B: 1]

Neither clock is a descendant of the other. History branched β€” exactly like two Git branches from the same commit.

gitGraph
  commit id: "A:1 milk"
  branch phone
  branch laptop
  checkout phone
  commit id: "A:2 +eggs"
  checkout laptop
  commit id: "A:1,B:1 +bacon"

Step 4 β€” Sync / read across replicas

When a client reads (or replicas gossip), the system compares clocks instead of timestamps.

Resolving the conflict

When two versions meet, compare their vector clocks component-wise.

Outcome 1 β€” Direct descendant (safe overwrite)

Clock X dominates clock Y if every counter in X is β‰₯ the same node's counter in Y, and at least one is strictly greater.

Example:

Old: [A: 1]
New: [A: 2]

[A: 2] dominates [A: 1] β†’ New is a direct descendant. Keep New. Discard Old. No conflict.

Another:

Old: [A: 2, B: 1]
New: [A: 2, B: 2]

Same story β€” New saw everything Old saw, plus one more write on B.

Outcome 2 β€” Divergence (return siblings)

Neither clock dominates the other:

[A: 2]          vs  [A: 1, B: 1]
  • A is ahead on itself (2 > 1)
  • B is ahead on itself (1 > 0)

Histories diverged. The database must not invent a merge:

  • Do not last-write-wins on wall time
  • Do not silently pick eggs or bacon
  • Return both versions (siblings) to the client

The application merges with domain rules. For a shopping cart, union is usually correct:

# Client / app merge
Version A: { milk, eggs }     clock [A: 2]
Version B: { milk, bacon }    clock [A: 1, B: 1]

Merged:    { milk, eggs, bacon }

Write the merge back through a coordinator (say C), which advances the clock by taking the element-wise max, then incrementing itself:

Cart:  { milk, eggs, bacon }
Clock: [A: 2, B: 1, C: 1]

That new clock dominates both parents. Future reads can treat it as the resolved tip β€” until the next concurrent fork.

Comparison cheat sheet

Compare X and Y element-wise across all nodes:

1. X β‰₯ Y on every node, and X > Y on at least one
   β†’ X descends from Y β†’ keep X

2. Y β‰₯ X on every node, and Y > X on at least one
   β†’ Y descends from X β†’ keep Y

3. Otherwise
   β†’ concurrent / divergent β†’ return both to the app

Why this matters in real systems

Vector clocks (and close cousins like dotted version vectors) show up wherever you accept eventual consistency and concurrent updates:

  • Dynamo / Riak-style key-value stores
  • Shopping carts, counters, presence, multi-device edits
  • Any CRDT-adjacent design where the DB detects conflict and the app (or a merge function) resolves it

Practical takeaways:

  • Timestamps order events on one machine; they do not prove causality across machines
  • Vector clocks detect concurrency; they do not merge business data
  • Conflict resolution is an application concern β€” union for carts, "keep both and ask the user" for docs, custom rules for money (often: avoid this model for money)

Recap

Approach What it tracks Concurrent write risk
Wall-clock LWW now() Silent data loss under clock skew
Vector clocks Per-node causal history Detects forks; client merges siblings

If you remember one line for interviews: vector clocks are version history, not time β€” and when histories branch, the database returns both versions instead of guessing.

Tony Duong

By Tony Duong

A digital diary. Thoughts, experiences, and reflections.