October 8, 2024 4 min read A to Z Series: V Home

Vector Clocks: Track Which Version Saw Which

A vector clock keeps one counter per participant so the system can tell whether one version includes another version or conflicts with it.

I use vector clocks when I need to know whether two updates are ordered or truly concurrent. Lamport time is useful, but vector clocks keep enough history to expose independent branches.

Conflict is not always a bug

When I talk about "Conflict is not always a bug", I am really asking what Vector Clocks does when a message is late, duplicated, or missing.

If a phone and laptop both edit the same shopping cart while offline, neither edit includes the other. A vector clock can show that the versions are concurrent, so the system can merge them or ask for help.

Profile update example

I use "Profile update example" to keep Vector Clocks grounded in a real system, because abstract patterns are too easy to agree with and too hard to operate.

A customer adds shoes to a cart on a phone and a jacket on a laptop during a network gap. The cart service receives both writes later. If it only stores the latest timestamp, one item can disappear. If it stores a vector clock with each cart version, the service can prove that both edits came from the same base version and happened independently, then merge both items into the cart.

One implementation shape

What I learnt around "One implementation shape" is that Vector Clocks only becomes clear when the repair path is visible.

A common implementation stores a small map with the record: participant name to the highest counter seen from that participant. Before a participant writes, it increments its own counter. When a service receives two versions, it compares every participant counter. If one clock is greater than or equal to the other everywhere, it safely replaces the older version. If each clock is greater in at least one place, the versions are concurrent and need merge logic.

Vector clock code

When I show "Vector clock code", I want the code in Vector Clocks to reveal the production decision, not just the syntax.

This Java sketch is the core of a practical vector clock: tick on write, merge on receive, and compare before deciding whether to overwrite or resolve a conflict.

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

enum Relation {
  BEFORE, AFTER, EQUAL, CONCURRENT
}

final class VectorClock {
  private final Map<String, Long> counters;

  VectorClock() {
    this.counters = Map.of();
  }

  private VectorClock(Map<String, Long> counters) {
    this.counters = Map.copyOf(counters);
  }

  VectorClock tick(String participant) {
    Map<String, Long> next = new HashMap<>(counters);
    next.put(participant, next.getOrDefault(participant, 0L) + 1);
    return new VectorClock(next);
  }

  VectorClock merge(VectorClock other) {
    Map<String, Long> merged = new HashMap<>(counters);
    other.counters.forEach((participant, value) ->
        merged.merge(participant, value, Math::max));
    return new VectorClock(merged);
  }

  Relation compare(VectorClock other) {
    boolean lower = false;
    boolean higher = false;

    Set<String> participants = new HashSet<>(counters.keySet());
    participants.addAll(other.counters.keySet());

    for (String participant : participants) {
      long left = counters.getOrDefault(participant, 0L);
      long right = other.counters.getOrDefault(participant, 0L);
      lower |= left < right;
      higher |= left > right;
    }

    if (lower && higher) return Relation.CONCURRENT;
    if (lower) return Relation.BEFORE;
    if (higher) return Relation.AFTER;
    return Relation.EQUAL;
  }

  @Override
  public String toString() {
    return counters.toString();
  }
}

class CartConflictExample {
  public static void main(String[] args) {
    VectorClock base = new VectorClock();

    VectorClock phoneEdit = base.tick("phone");
    VectorClock laptopEdit = base.tick("laptop");

    if (phoneEdit.compare(laptopEdit) == Relation.CONCURRENT) {
      VectorClock mergedCart = phoneEdit.merge(laptopEdit).tick("cartService");
      System.out.println("Merged cart clock: " + mergedCart);
    }
  }
}

The comparison detects concurrent edits from two devices and merges their clocks after conflict handling. That is the practical value of vector clocks: spotting work that did not happen before or after the other work.

Sequence diagram: detect concurrent cart edits

detect concurrent cart edits The clocks show that neither edit replaced the other. They happened on separate branches. sd detect concurrent cart edits Phone Phone Cart Copy Cart Copy Laptop Laptop Phone andLaptop Phone andLaptop Cart Service Cart Service Merged Cart Merged Cart Add shoes and incrementphone counter. Add jacket and incrementlaptop counter. Sync both versionslater. Detect concurrency andkeep both items.

The clocks show that neither edit replaced the other. They happened on separate branches.

The conflict rule

Next ordering reads

Written by Arunkumar Ganesan.

What I learnt is that conflict handling improves when the system can say these edits happened independently instead of guessing from timestamps.

#DistributedSystems #VectorClocks #ConflictResolution #Architecture