<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Distributed System on Dat a Engineer</title><link>https://note.datengineer.dev/tags/distributed-system/</link><description>Recent content in Distributed System on Dat a Engineer</description><image><title>Dat a Engineer</title><url>https://note.datengineer.dev/images/cover.png</url><link>https://note.datengineer.dev/images/cover.png</link></image><generator>Hugo -- 0.147.5</generator><language>en-us</language><lastBuildDate>Wed, 18 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://note.datengineer.dev/tags/distributed-system/index.xml" rel="self" type="application/rss+xml"/><item><title>Handling Concurrent Inserts: From Single Database to Distributed</title><link>https://note.datengineer.dev/posts/handling-concurrent-inserts-from-single-database-to-distributed/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://note.datengineer.dev/posts/handling-concurrent-inserts-from-single-database-to-distributed/</guid><description>Description</description><content:encoded><![CDATA[<p>During a recent technical meeting, one of my colleagues raised a concern about two concurrent inserts for the same object from different processes hitting the system at once. After explaining this to him, I realized that even among experienced engineers, the inner workings of the database are often treated as a black box. There is a persistent uncertainty about what the database guarantees and what it leaves up to developers.</p>
<h2 id="problem-statement">Problem Statement</h2>
<h3 id="user-registration-flow">User Registration Flow</h3>
<p>To understand the concurrent inserts problem, the most common example is the user registration flow. When a user reaches our system, they first select a username. The system must guarantee that this username belongs to one and only one person.</p>
<p><img alt="User Registration Flow" loading="lazy" src="/posts/handling-concurrent-inserts-from-single-database-to-distributed/images/database-concurrent-insert-demo-user-registration-flow.png"></p>
<h3 id="check-then-act">Check-Then-Act</h3>
<p>After you see the requirement, you sit down to code immediately. You simply follow a linear logical path. You first check if the username exists in the database. If the username exists, you throw an error to users. Otherwise, you insert a new user record to the database. So, you just implemented what is known as the Check-Then-Act pattern. The code usually looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Pseudo Python code</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">register</span><span class="p">(</span><span class="n">username</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Step 1: The Check</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># We query the database to see if the username is already claimed.</span>
</span></span><span class="line"><span class="cl">    <span class="n">existing_user</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;SELECT id FROM users WHERE username = </span><span class="si">%s</span><span class="s2">&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">username</span><span class="p">,))</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">existing_user</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Step 2: The Act</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># If the result is empty, we assume the coast is clear and perform the write.</span>
</span></span><span class="line"><span class="cl">        <span class="n">db</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;INSERT INTO users (username) VALUES (</span><span class="si">%s</span><span class="s2">)&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">username</span><span class="p">,))</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">&#34;Registration successful.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">&#34;Error: Username already exists.&#34;</span>
</span></span></code></pre></div><p>This pattern is incredibly common because it is intuitive. It is how we think and act in real life. We check if a parking spot is empty before we pull the car in. It is how we are taught to code at school. And it looks the same as the received requirements.</p>
<h3 id="the-race-condition">The Race Condition</h3>
<p>The fatal flaw in &ldquo;Check-Then-Act&rdquo; is the tiny window of time between the &ldquo;Check&rdquo; and the &ldquo;Act&rdquo;. Imagine that two users, Alice and Bob, both try to claim the username &ldquo;user1&rdquo; at the same exact millisecond.</p>
<ul>
<li>Thread A (Alice) checks the database and sees that &ldquo;user1&rdquo; is available.</li>
<li>Thread B (Bob) also checks the database, but a fraction of a second before Thread A commits its write. Thread B also sees that &ldquo;user1&rdquo; is available.</li>
<li>Both threads then proceed to Insert.</li>
</ul>
<p>You now have two rows in your table with the username &ldquo;user1&rdquo;. Your authentication and authorization logic could be broken.</p>
<p><img alt="User Registration Race Condition Example" loading="lazy" src="/posts/handling-concurrent-inserts-from-single-database-to-distributed/images/database-concurrent-insert-conflict-race-condition-example.png"></p>
<p>The &ldquo;Check-Then-Act&rdquo; pattern is even more fragile in a production environment because a single database can hardly handle massive real-world loads. For this reason, we often distribute the load across multiple databases. In the simplest setup, all writes are routed to a leader database, and all reads are distributed among replicas. It means that the &ldquo;Check&rdquo; may interact with one database while the &ldquo;Act&rdquo; is performed on another. And the replication lag between these databases makes the race condition more likely to happen.</p>
<h2 id="how-to-insert">How to <code>INSERT</code>?</h2>
<p>To find the solution, we first need to understand one important aspect of <code>INSERT</code>. When inserting data, we usually provide values for all columns except the primary key. We often rely on databases to generate an auto-increment value for us. Two update statements could possibly block if they interact with the same primary key. But two insert statements would never do so because they always use different primary keys. Therefore, two concurrent inserts always succeed and will cause duplication.</p>
<p>The above behavior gives us an insightful clue. No matter how many concurrent inserts hit your database, it guarantees that every new row gets a distinct ID. And the fact that the database assign IDs in auto-increment order (e.g., 1, 2, 3, 4, 5, etc.) tells us it has a mechanism to process these IDs sequentially, even if insert requests may arrive at the same time.</p>
<h3 id="unique-constraint">Unique Constraint</h3>
<p>The problem is not the database cannot handle simultaneous inserts. The problem is we must tell the database that usernames require the same strict treatment as IDs. And we can solve this by simply adding a UNIQUE constraint to the username column.</p>
<p>Thanks to the unique constraint, duplication is no longer a concern. The database will ensure that, at any given time, no two users share the same username.</p>
<h3 id="just-insert">Just <code>INSERT</code></h3>
<p>Once the UNIQUE constraint has been set up, you can insert data without hesitation. We no longer need existence checks beforehand. We abandon the &ldquo;Check-Then-Act&rdquo; pattern entirely. We just insert.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Pseudo Python code</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">register</span><span class="p">(</span><span class="n">username</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Just try to insert. </span>
</span></span><span class="line"><span class="cl">        <span class="c1"># The database is the only one who knows the truth.</span>
</span></span><span class="line"><span class="cl">        <span class="n">db</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;INSERT INTO users (username) VALUES (</span><span class="si">%s</span><span class="s2">)&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">username</span><span class="p">,))</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">&#34;Success&#34;</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">    <span class="k">except</span> <span class="n">UniqueViolationError</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># The database rejected us. </span>
</span></span><span class="line"><span class="cl">        <span class="c1"># This is a valid business logic outcome, not a system crash.</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="s2">&#34;Error: Username already exists.&#34;</span>
</span></span></code></pre></div><p>Databases will handle our concurrent inserts as if they were executed sequentially. So, Alice and Bob both try to register the username &ldquo;user1.&rdquo; Their requests arrive at the same time. One request will be executed first and succeed. The other will wait, then violate the unique constraint and fail. We will catch that failure and prompt the user to choose another username.</p>
<h2 id="multi-leader">Multi-Leader</h2>
<p>The &ldquo;Just Insert&rdquo; approach is clean. It works perfectly in a single-leader setup. So, we ship it and life is good.</p>
<p>Then, the product grows.</p>
<h3 id="why-multi-leader">Why Multi-Leader?</h3>
<p>A single leader database can only handle a certain number of writes per second. When users register, post, and update their profiles all at once, the database becomes the bottleneck. You also start thinking about geography. A user in Tokyo shouldn&rsquo;t have to wait for a round trip to a server in London just to register. Latency adds up. Users leave.</p>
<p>So, you scale out. Promote multiple nodes to accept writes. Each region has its own leader database that accepts writes. Writes go to the nearest database. Then, the leaders synchronize with each other in the background. This is called multi-leader replication.</p>
<p>Reads are fast. Writes are fast. Everything feels great. Until you think about usernames again.</p>
<h3 id="why-unique-not-work">Why UNIQUE not work?</h3>
<p>The UNIQUE guarantee is only valid where there is one database. In a multi-leader setup, Leader A in Tokyo and Leader B in London are both accepting writes independently. They do not talk to each other before committing. Alice registers &ldquo;user1&rdquo; on Leader A. Bob registers &ldquo;user1&rdquo; on Leader B. Both leaders check their own local data. Both see no conflict. Both succeed. Both return a success response to their respective users.</p>
<p>Now both leaders have a row for &ldquo;user1&rdquo;. The UNIQUE constraint on each node was never violated locally. The violation only becomes visible after the leaders then sync with each other.</p>
<h3 id="solutions">Solutions</h3>
<p>There is no magic answer here. Every solution is a trade-off between consistency, availability, and difficulty. You have to pick one that works best for you.</p>
<p>The simplest method is optimistic conflict resolution. You allow conflict. No locks. No coordination. No waiting. Every INSERT request succeeds instantly. And you change business requirement to resolve conflicts.</p>
<p>The most common strategy is Last-Write-Win. For the user registration problem, you might want to use First-Write-Win. The idea is simple, you keep one and discard others. You can also keep all, but with additional discriminators to enforce uniqueness. Discord used to take this approach. They appended a 4-digit numeric discriminator to every username. So that, in theory, they allowed 10,000 users sharing the username &ldquo;user1&rdquo;, ranging from &ldquo;user1#0000&rdquo; to &ldquo;user1#9999&rdquo;.</p>
<p>The fundamental problem with this approach is the user experience. In most cases, such as when a user updates their profile image from both their laptop and phone at the same time, it is safe to keep the latest one. However, this is not as easy with uniqueness-sensitive data, such as usernames. You definitely do not want to tell a user that they have successfully taken a username. Then, when they log in the next time, you have to apologize because you found out that someone had taken that name before them. Even the discriminator is also a poor user experience. No one remembers a random number associated with their name. Discord has finally decided to move away from that approach.</p>
<p>Another method is to stop fighting the problem of multi-leader setup and route uniqueness-sensitive data to a dedicated service backed by a single-leader database. So, you can keep everything else in the multi-leader system for performance, but send user registration to the single-leader database. Your main database stays distributed and fast. Only the username is centralized, because it is the only part that genuinely needs to be.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>The moment you start treating the database as a partner rather than a black box, a lot of problems either disappear entirely or become much easier to reason about. You stop being surprised by behaviors that are actually well-documented. You start asking better questions. Understanding your tools in depth is especially important in the era of AI. AI can provide you code that looks correct. But the decision of whether that code is actually correct is still yours.</p>
]]></content:encoded></item><item><title>Snowflake ID - Simplifying uniqueness in distributed systems</title><link>https://note.datengineer.dev/posts/snowflake-id-simplifying-uniqueness-in-distributed-systems/</link><pubDate>Sat, 03 Feb 2024 00:00:00 +0000</pubDate><guid>https://note.datengineer.dev/posts/snowflake-id-simplifying-uniqueness-in-distributed-systems/</guid><description>Guide to generating unique IDs in distributed data systems.</description><content:encoded><![CDATA[<h2 id="problem-description">Problem description</h2>
<p>In developing database systems, generating IDs is a crucial task. IDs ensure the uniqueness of data, facilitate queries, and establish relationship constraints in databases. Most modern database management systems (DBMS) can generate auto-increment IDs. We can delegate this task to the DBMS entirely and not worry about the uniqueness. However, there are several reasons why we shouldn&rsquo;t use auto-increment IDs, especially for distributed systems. The most important reason is that in distributed systems with independent servers, using per-server auto-increment IDs does not guarantee uniqueness and can lead to duplication problems.</p>
<p><a href="https://developer.twitter.com/en/docs/basics/twitter-ids">Snowflake ID</a> is the solution developed by Twitter engineers to address this problem. According to statistics, about 6,000 tweets are written and posted on Twitter every second. How can we generate 6,000 IDs per second independently on multiple servers without collision?</p>
<h2 id="hold-on-what-about-uuid">Hold on! What about UUID?</h2>
<p>UUID is also a widely used ID generation technique that has been used in software for a long time.</p>
<p>The idea of this technique is to use a 128-bit number as an ID. A 128-bit integer means that if we use 6,000 IDs every second, it will take over <code>2^128 / (6000 * 3600 * 24 * 365) = 1.79838*10^27</code> <em>(how is it pronounced, octillion?)</em> to exhaust them. And if we randomly pick 103 trillion of 126-bit numbers, the chance of a collision is one in a billion. Of course, this number is not generated in a simple incremental count like 1, 2, 3, &hellip; but follows a certain standard. And when generated accordingly, UUIDs can solve the problem of generating non-duplicate IDs in distributed systems.</p>
<p>But it introduces another problem. 128-bits is unnecessarily large. And most computers don&rsquo;t support us working directly with the 128-bit integer data type. So we usually have to use strings to process them. In addition, a large-size ID hurts the query performance because the index gets larger and operations/calculations become more costly.</p>
<p><em>Example UUIDs:</em></p>
<pre tabindex="0"><code>8fbb69e1-2132-4c86-911b-4cc182a5513a
b1f352c3-2126-4cca-9eec-349cdb69b611
6c7fa5c6-1b70-4b47-8690-760f2871943d
df495b1b-cd86-4e08-a42a-9f73d2c5afd1
13e1b2c2-ed6e-46ee-94a3-efce635ef268
</code></pre><h2 id="snowflake-id">Snowflake ID</h2>
<p>To solve this problem, Twitter engineers introduced a system called Snowflake ID. The idea of this system is to programmatically generate a 64-bit integer to represent the ID. But how does each server independently generate this ID without collision?</p>
<p>The proposed method is as follows:</p>
<ul>
<li>The first 1 bit is not in use (always 0) to make it fit into a signed integer (always positive).</li>
<li>The next 41 bits store information about the ID creation time, measured in milliseconds from a given point in time (epoch 1288834974657 in Unix time).</li>
<li>The next 10 bits store information about the machine requesting the ID.</li>
<li>The last 12 bits are a sequential counter from 1 to 4096. To avoid duplicate IDs within the same time frame on the same machine.</li>
</ul>
<p><img alt="Snowflake ID Generate ID for database" loading="lazy" src="/posts/snowflake-id-simplifying-uniqueness-in-distributed-systems/images/snowflake-id-format-id-generation-distributed-system-min.png"></p>
<p>The only scenario where collisions can occur is when a single machine requests more than 4096 IDs in a single millisecond (or in the case of Twitter, when a machine posts 4096 tweets in a millisecond). ⁤With Snowflake ID, we are able to solve the problem of generating non-duplicate IDs in distributed systems with a 64-bit integer. ⁤⁤Additionally, Snowflake ID itself contains information about the creation time. ⁤⁤Therefore, we can know the creation time of an ID just by looking at the ID, or we can sort by ID and get results sorted by time. ⁤</p>
<h2 id="conclusion">Conclusion</h2>
<p>Is Snowflake ID the answer for every system? Absolutely not! <strong>Nothing is the answer for everything</strong>. Besides the ID generation strategies mentioned above, there are many other approaches (e.g. Flickr&rsquo;s centralized ticket server). There are always many ways to solve a problem. Each goes with pros and cons. Don&rsquo;t limit yourself to existing methods; always look for new, context-appropriate solutions.</p>
]]></content:encoded></item></channel></rss>