<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anton Lungameni</title>
    <description>The latest articles on DEV Community by Anton Lungameni (@antonlungameni).</description>
    <link>https://dev.to/antonlungameni</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1463936%2Ff873cb2f-529d-4b0a-9191-254798a0ce51.jpeg</url>
      <title>DEV Community: Anton Lungameni</title>
      <link>https://dev.to/antonlungameni</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/antonlungameni"/>
    <language>en</language>
    <item>
      <title>Vector Clocks, Explained With a Budget</title>
      <dc:creator>Anton Lungameni</dc:creator>
      <pubDate>Fri, 09 Oct 2026 21:28:37 +0000</pubDate>
      <link>https://dev.to/antonlungameni/vector-clocks-explained-with-a-budget-3igd</link>
      <guid>https://dev.to/antonlungameni/vector-clocks-explained-with-a-budget-3igd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; When the same data is edited on several devices, "keep the newest" is&lt;br&gt;
unreliable, because device clocks disagree and messages arrive late. Vector clocks record&lt;br&gt;
what each device had &lt;em&gt;seen&lt;/em&gt;, so a system can tell a genuine conflict from an update that&lt;br&gt;
simply arrived late. I explain them with a budget, plus three bugs that taught me to handle&lt;br&gt;
all four possible outcomes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My budget lives on a desktop, a laptop and a VPS. Suppose the grocery limit is edited on the&lt;br&gt;
laptop, and later a change to the same limit arrives from the desktop. Should the incoming&lt;br&gt;
change replace the one I have?&lt;/p&gt;

&lt;p&gt;The obvious answer is "compare timestamps, keep the newer one". It's wrong, for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clocks disagree.&lt;/strong&gt; The laptop's clock can be minutes off the desktop's. "Newer by the
timestamp" can mean "made on the machine whose clock runs fast".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrival order isn't creation order.&lt;/strong&gt; A change made yesterday can arrive after a change
made today, if the machine that made it was offline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I actually need to know is not &lt;em&gt;when&lt;/em&gt; each change happened, but whether one change&lt;br&gt;
&lt;strong&gt;knew about&lt;/strong&gt; the other. That's what a vector clock records.&lt;/p&gt;
&lt;h2&gt;
  
  
  A vector clock is a counter per machine
&lt;/h2&gt;

&lt;p&gt;Every change carries a small map: for each machine, how many of that machine's changes had&lt;br&gt;
been seen when this change was made.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{desktop: 2, laptop: 1}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;reads: "this change was made by a machine that had seen the desktop's first two changes and&lt;br&gt;
the laptop's first one."&lt;/p&gt;

&lt;p&gt;Three operations are all there is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local change:&lt;/strong&gt; take the current clock and add one to &lt;em&gt;your own&lt;/em&gt; counter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receive:&lt;/strong&gt; merge by taking the &lt;strong&gt;maximum&lt;/strong&gt; of each counter (not the sum: summing would
invent changes that never happened).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare&lt;/strong&gt; two clocks, counter by counter.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Comparing: four answers, not two
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vc&lt;/span&gt; &lt;span class="n"&gt;VectorClock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="n"&gt;VectorClock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ClockRelation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;hasLess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasGreater&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;nodeID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;vc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;nodeID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;nodeID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// a missing key reads as 0&lt;/span&gt;
            &lt;span class="n"&gt;hasLess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;hasGreater&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;hasLess&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;hasGreater&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Concurrent&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;hasLess&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Before&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;hasGreater&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;After&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Equal&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comparing over the &lt;strong&gt;union&lt;/strong&gt; of both clocks' keys matters: a machine missing from one&lt;br&gt;
clock counts as zero, and both sides reach the same answer however the maps are built.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stored&lt;/th&gt;
&lt;th&gt;Incoming&lt;/th&gt;
&lt;th&gt;Relation&lt;/th&gt;
&lt;th&gt;What it means for the grocery limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{desktop:1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{desktop:2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before&lt;/td&gt;
&lt;td&gt;The incoming edit saw mine and changed it further: &lt;strong&gt;apply it&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{desktop:2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{desktop:1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After&lt;/td&gt;
&lt;td&gt;The incoming edit is older, arriving late: &lt;strong&gt;ignore it&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{desktop:2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{desktop:2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Equal&lt;/td&gt;
&lt;td&gt;Same edit, delivered twice: &lt;strong&gt;ignore it&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{desktop:1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{laptop:1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Concurrent&lt;/td&gt;
&lt;td&gt;Neither saw the other: a &lt;strong&gt;real conflict&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the last row. &lt;code&gt;{laptop:1}&lt;/code&gt; isn't "less" than &lt;code&gt;{desktop:2}&lt;/code&gt; just because 1 &amp;lt; 2. A clock&lt;br&gt;
is newer only if it's at least as large in &lt;strong&gt;every&lt;/strong&gt; counter. One bigger number means&lt;br&gt;
nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: treating it as a yes/no question
&lt;/h2&gt;

&lt;p&gt;My first version of the sync handler asked only "is this a conflict?" Anything that&lt;br&gt;
wasn't concurrent got applied. That merged two very different cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before&lt;/strong&gt;: the incoming edit is newer. Applying it is right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After&lt;/strong&gt;: the incoming edit is &lt;em&gt;older&lt;/em&gt;, delivered late. Applying it silently reverts my
budget to stale data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is precisely the situation vector clocks exist for, and my code threw the answer away.&lt;br&gt;
The fix was to handle all four relations explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: comparing a change to itself
&lt;/h2&gt;

&lt;p&gt;An early version compared the incoming change with... the incoming change. A clock is always&lt;br&gt;
&lt;code&gt;Equal&lt;/code&gt; to itself, so the conflict branch could never run. The code compiled, the tests&lt;br&gt;
passed, and conflict detection was dead code. The tests only checked cases where no conflict&lt;br&gt;
was expected, so nothing noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: a local edit that didn't move forward
&lt;/h2&gt;

&lt;p&gt;When I make a change on the laptop, the new clock must come &lt;em&gt;after&lt;/em&gt; everything the laptop&lt;br&gt;
already has for that item. The rule is: &lt;strong&gt;copy the item's current clock, then increment&lt;br&gt;
your own counter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I hit both ways of getting this wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Starting from an empty clock.&lt;/strong&gt; My second edit to an item looked &lt;code&gt;Concurrent&lt;/code&gt; with my
first, a conflict with myself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to copy.&lt;/strong&gt; In Go, a map inside a struct is shared when the struct is copied.
Incrementing "my copy" of the clock incremented the stored one too, so old and new compared
&lt;code&gt;Equal&lt;/code&gt;, and the other machine ignored the edit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this shows up in real systems
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline-first apps.&lt;/strong&gt; Field-service, inspection, sales and healthcare apps used where
connectivity is patchy all face the same question when a device reconnects: is this change
newer, older, or in conflict with what the server has?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replicated databases.&lt;/strong&gt; Amazon's Dynamo paper used vector clocks to detect conflicting
writes across replicas, and Riak built on the same idea.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anywhere "last save wins" is the default.&lt;/strong&gt; Multi-device note-taking, CRMs, shared
spreadsheets: without causality tracking, a late-arriving save silently overwrites newer work,
and users call it data loss.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Wall-clock time answers "when"; distributed systems usually need "what did you know".&lt;/li&gt;
&lt;li&gt;A comparison with four outcomes deserves four branches. Collapsing them is how stale data
wins.&lt;/li&gt;
&lt;li&gt;When a test only covers the cases where nothing interesting happens, it can pass forever
while the interesting code never runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Next: Retries Are Normal: Building an Idempotent Sync API.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;About this series: I'm learning distributed systems by building a real application, with an&lt;br&gt;
AI assistant as coach and pair programmer. It explains, reviews and sometimes writes code; I&lt;br&gt;
verify every claim against tests and measurements, and every bug and number here is real.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write about building reliable software in Go, from the code up. If you're working on similar&lt;br&gt;
problems, I'd like to hear from you: &lt;a href="https://lungameni.dev/contact" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt; or connect with me on LinkedIn.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>vectorclocks</category>
      <category>go</category>
      <category>causality</category>
    </item>
  </channel>
</rss>
