<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Engineering Deep Dives]]></title><description><![CDATA[Deep dives into backend engineering, distributed systems, developer tooling, performance, security, and software architecture. Practical insights, production-ready patterns, and engineering trade-offs.]]></description><link>https://engineeringdeepdives.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/63e5cf1af30581f89abcd1e1/be683542-474d-4216-97b7-e2437d2e24aa.png</url><title>Engineering Deep Dives</title><link>https://engineeringdeepdives.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 07 Sep 2026 02:14:31 GMT</lastBuildDate><atom:link href="https://engineeringdeepdives.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Foundations of System Design: Learn It the Way I Wish Someone Taught Me]]></title><description><![CDATA[When I first started learning system design, I felt overwhelmed.
Load balancers. Caches. Message queues. Replication. Sharding. Distributed systems.
There are a lot of terms. And most guides throw the]]></description><link>https://engineeringdeepdives.hashnode.dev/foundations-of-system-design-learn-it-the-way-i-wish-someone-taught-me</link><guid isPermaLink="true">https://engineeringdeepdives.hashnode.dev/foundations-of-system-design-learn-it-the-way-i-wish-someone-taught-me</guid><category><![CDATA[backend]]></category><category><![CDATA[api]]></category><category><![CDATA[System Design]]></category><dc:creator><![CDATA[Noor ul Hassan]]></dc:creator><pubDate>Sun, 06 Sep 2026 13:45:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/63e5cf1af30581f89abcd1e1/28e50a8e-2556-45dd-8b6d-c40c01b6b54b.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I first started learning system design, I felt overwhelmed.</p>
<p>Load balancers. Caches. Message queues. Replication. Sharding. Distributed systems.</p>
<p>There are a lot of terms. And most guides throw them at you like a vocabulary test.</p>
<p>But here is the thing: I don't think the best way to learn system design is to memorize diagrams or vocabulary.</p>
<p>I prefer starting with something much simpler:</p>
<blockquote>
<p><strong>Start with one request. Then introduce a new concept only when the system actually needs it.</strong></p>
</blockquote>
<p>That is what we are going to do in this guide.</p>
<p>We will start with a very small application and gradually make it capable of handling more traffic and more failures. Every new concept we introduce will be because the system actually broke and needed it.</p>
<p>If you have ever felt like system design is "too advanced" or "not for you" yet, I promise you it is. You just need the right entry point.</p>
<p>Let's build it together.</p>
<hr />
<h2>What We Will Cover</h2>
<ul>
<li><p>What system design actually means (and why it is not just for senior engineers)</p>
</li>
<li><p>How a browser request travels to your server</p>
</li>
<li><p>Where the database fits and why relational databases are useful</p>
</li>
<li><p>Transactions and why they matter</p>
</li>
<li><p>Scaling: vertical and horizontal</p>
</li>
<li><p>Load balancers and how they distribute traffic</p>
</li>
<li><p>Health checks and detecting failed servers</p>
</li>
<li><p>Redundancy and why it matters</p>
</li>
<li><p>The architecture we built and the problems we have not solved yet</p>
</li>
<li><p>The mental model for approaching any system design problem</p>
</li>
</ul>
<p>Let's start.</p>
<hr />
<h2>What Is System Design?</h2>
<p>In simple terms, <strong>system design is about deciding how the different parts of a software system should work together as the system grows.</strong></p>
<p>When your application has 10 users, almost any reasonable architecture can work.</p>
<p>When it has 100,000 users, things start getting interesting.</p>
<p>Your server has limited CPU and memory.</p>
<p>Your database has limits.</p>
<p>Networks fail.</p>
<p>Servers crash.</p>
<p>Requests take longer than expected.</p>
<p>Two users can try to change the same data at the same time.</p>
<p>So system design is not just:</p>
<blockquote>
<p>"How do I make this application work?"</p>
</blockquote>
<p>It becomes:</p>
<blockquote>
<p><strong>"How do I make this application continue working as traffic, data, and failures increase?"</strong></p>
</blockquote>
<p>If that question excites you, you are in the right place.</p>
<hr />
<h2>A Request From the Browser</h2>
<p>Imagine you have a frontend application running in the browser.</p>
<p>The browser needs a list of products.</p>
<p>It sends a request:</p>
<pre><code class="language-text">Browser
   |
   | GET /products
   ↓
api.example.com
</code></pre>
<p>But <code>api.example.com</code> is not the server's IP address.</p>
<p>It is a <strong>domain name</strong>.</p>
<p>The browser needs to find the IP address associated with that domain.</p>
<p>This is where <strong>DNS (Domain Name System)</strong> comes in.</p>
<p>Very roughly:</p>
<pre><code class="language-text">Browser
   |
   | "Where is api.example.com?"
   ↓
DNS
   |
   | "It is available at this IP"
   ↓
IP address
   |
   ↓
Server
</code></pre>
<p>DNS is basically the system that helps translate human-readable domain names into network addresses.</p>
<p>You do not need to remember the entire DNS process yet.</p>
<p>For now, just remember:</p>
<blockquote>
<p><strong>Domain name → DNS resolution → IP address → server</strong></p>
</blockquote>
<hr />
<h2>The Server Processes the Request</h2>
<p>Now the request reaches our server.</p>
<p>The server receives:</p>
<pre><code class="language-http">GET /products
</code></pre>
<p>It needs to retrieve the products from the database.</p>
<p>So our system currently looks like this:</p>
<pre><code class="language-text">Client
   |
   | Request
   ↓
Server
   |
   | Query
   ↓
Database
</code></pre>
<p>The database returns the data:</p>
<pre><code class="language-text">Database
   |
   | Products
   ↓
Server
   |
   | JSON response
   ↓
Client
</code></pre>
<p>The complete flow is:</p>
<pre><code class="language-text">Client
   ↓
Server
   ↓
Database
   ↓
Server
   ↓
Client
</code></pre>
<p>At this point, everything looks simple.</p>
<p>And that is exactly what we want.</p>
<blockquote>
<p><strong>Do not introduce a load balancer, Redis, Kafka, Kubernetes and 15 other things when one server is perfectly capable of handling the application.</strong></p>
</blockquote>
<hr />
<h2>Where Does the Database Fit?</h2>
<p>Let us say our application has these entities:</p>
<pre><code class="language-text">Users
Products
Orders
Accounts
Sessions
</code></pre>
<p>This data has structure.</p>
<p>A user has an ID, name and email.</p>
<p>A product has an ID, name and price.</p>
<p>An order belongs to a user and contains products.</p>
<p>This is where a <strong>relational database</strong> can be a very good fit.</p>
<p>Examples include:</p>
<ul>
<li><p>PostgreSQL</p>
</li>
<li><p>MySQL</p>
</li>
<li><p>Oracle</p>
</li>
<li><p>SQLite</p>
</li>
</ul>
<p>Relational databases organize data into <strong>tables</strong>, which contain <strong>rows</strong> and <strong>columns</strong>.</p>
<p>For example:</p>
<p><strong>users</strong></p>
<ul>
<li><p><code>id</code>: 1</p>
</li>
<li><p><code>name</code>: Noor</p>
</li>
<li><p><code>email</code>: <a href="mailto:noor@example.com">noor@example.com</a></p>
</li>
<li><p><code>id</code>: 2</p>
</li>
<li><p><code>name</code>: Ali</p>
</li>
<li><p><code>email</code>: <a href="mailto:ali@example.com">ali@example.com</a></p>
</li>
</ul>
<p>And:</p>
<p><strong>products</strong></p>
<ul>
<li><p><code>id</code>: 1</p>
</li>
<li><p><code>name</code>: Keyboard</p>
</li>
<li><p><code>price</code>: 100</p>
</li>
<li><p><code>id</code>: 2</p>
</li>
<li><p><code>name</code>: Mouse</p>
</li>
<li><p><code>price</code>: 50</p>
</li>
</ul>
<p>The important thing is not memorizing the names of databases.</p>
<p>The important question is:</p>
<blockquote>
<p><strong>What kind of data and guarantees does my application need?</strong></p>
</blockquote>
<hr />
<h2>Why Relational Databases Are Useful</h2>
<p>One major advantage of relational databases is that they let us model relationships between data.</p>
<p>Suppose Noor buys a keyboard.</p>
<p>We might have:</p>
<pre><code class="language-text">users
  ↓
orders
  ↓
order_items
  ↓
products
</code></pre>
<p>The database can represent those relationships explicitly.</p>
<p>We can then query information such as:</p>
<blockquote>
<p>Which products did this user purchase?</p>
</blockquote>
<p>This is where SQL and operations such as <code>JOIN</code> become useful.</p>
<p>For example, conceptually:</p>
<pre><code class="language-sql">SELECT users.name, products.name
FROM users
JOIN orders ON orders.user_id = users.id
JOIN order_items ON order_items.order_id = orders.id
JOIN products ON products.id = order_items.product_id;
</code></pre>
<p>You do not need to memorize this query.</p>
<p>The important idea is that relational databases are very good when your data has clear relationships and you need strong consistency around those relationships.</p>
<hr />
<h2>Transactions</h2>
<p>Now we have reached our first really important system-design problem.</p>
<p>Imagine a user places an order.</p>
<p>Two things need to happen:</p>
<pre><code class="language-text">1. Create the order
2. Deduct the user's balance
</code></pre>
<p>What happens if step 1 succeeds but step 2 fails?</p>
<p>We could end up with:</p>
<pre><code class="language-text">Order created
Money not deducted
</code></pre>
<p>That is an invalid state for many applications.</p>
<p>This is where <strong>transactions</strong> become important.</p>
<p>A transaction allows us to group multiple database operations into one logical operation.</p>
<p>Conceptually:</p>
<pre><code class="language-text">BEGIN TRANSACTION

Create order
Deduct balance

COMMIT
</code></pre>
<p>If something goes wrong:</p>
<pre><code class="language-text">BEGIN TRANSACTION

Create order
Deduct balance ← FAIL

ROLLBACK
</code></pre>
<p>The database can roll back the changes made by the transaction.</p>
<p>The simple mental model is:</p>
<blockquote>
<p><strong>Either the required operations succeed together, or the transaction does not commit their changes.</strong></p>
</blockquote>
<p>Transactions are built around the <strong>ACID</strong> properties:</p>
<pre><code class="language-text">A → Atomicity
C → Consistency
I → Isolation
D → Durability
</code></pre>
<p>You do not need to master ACID right now.</p>
<p>Just understand why transactions exist:</p>
<blockquote>
<p><strong>They help us keep related database operations in a valid state when something fails or when multiple operations happen concurrently.</strong></p>
</blockquote>
<hr />
<h2>Our First Bottleneck</h2>
<p>So far we have:</p>
<pre><code class="language-text">                  ┌──────────┐
Client ────────→ │  Server  │
                  └────┬─────┘
                       │
                       ↓
                  ┌──────────┐
                  │ Database │
                  └──────────┘
</code></pre>
<p>Now imagine our application becomes popular.</p>
<p>We start getting:</p>
<pre><code class="language-text">10 requests/second
</code></pre>
<p>Then:</p>
<pre><code class="language-text">100 requests/second
</code></pre>
<p>Then:</p>
<pre><code class="language-text">1,000 requests/second
</code></pre>
<p>Our server has limited resources.</p>
<p>It has:</p>
<pre><code class="language-text">CPU
RAM
Network
Disk
</code></pre>
<p>At some point, one machine may no longer be enough.</p>
<p>Now we have to think about scaling.</p>
<p>This is where system design starts becoming interesting.</p>
<hr />
<h2>Vertical Scaling</h2>
<p>The first thing we can do is make the existing server stronger.</p>
<p>Maybe we currently have:</p>
<pre><code class="language-text">4 CPU cores
8 GB RAM
</code></pre>
<p>We could upgrade it to:</p>
<pre><code class="language-text">16 CPU cores
64 GB RAM
</code></pre>
<p>This is called <strong>vertical scaling</strong>.</p>
<p>In simple terms:</p>
<blockquote>
<p><strong>Make the machine bigger.</strong></p>
</blockquote>
<p>It is easy to understand and often the simplest solution.</p>
<p>But it has limits.</p>
<h3>Problem 1: Hardware Has Limits</h3>
<p>You cannot infinitely increase the CPU and RAM of one machine.</p>
<p>Eventually you reach the limits of the available hardware or the cost becomes unreasonable.</p>
<h3>Problem 2: Single Point of Failure</h3>
<p>We still have one server.</p>
<p>If that server goes down:</p>
<pre><code class="language-text">Server crashes
   ↓
Application unavailable
</code></pre>
<p>This is a <strong>single point of failure</strong>.</p>
<p>A single point of failure is a component whose failure can bring down an important part of the system.</p>
<p>So we have solved one problem but created another question:</p>
<blockquote>
<p><strong>Can we run multiple servers?</strong></p>
</blockquote>
<hr />
<h2>Horizontal Scaling</h2>
<p>Instead of making one server bigger, we can add more servers.</p>
<p>For example:</p>
<pre><code class="language-text">         Server 1
       /
Client ── Server 2
       \
         Server 3
</code></pre>
<p>This is called <strong>horizontal scaling</strong>.</p>
<p>Instead of:</p>
<pre><code class="language-text">1 very powerful server
</code></pre>
<p>we have:</p>
<pre><code class="language-text">3 servers
</code></pre>
<p>Now if one server fails, the other servers may still be able to handle requests.</p>
<p>But we have created a new problem.</p>
<p>How does the client know which server to talk to?</p>
<p>We do not want clients manually choosing:</p>
<pre><code class="language-text">server-1.example.com
server-2.example.com
server-3.example.com
</code></pre>
<p>We need something in front of them.</p>
<hr />
<h2>Load Balancer</h2>
<p>This is where a <strong>load balancer</strong> comes in.</p>
<p>A load balancer receives incoming requests and decides which server should handle each request.</p>
<p>Our architecture becomes:</p>
<pre><code class="language-text">                    ┌──────────┐
                    │ Server 1 │
                  /
Client → Load Balancer → Server 2
                  \
                    └──────────┘
                         Server 3
</code></pre>
<p>The client does not need to know about all the application servers.</p>
<p>It talks to the load balancer.</p>
<p>The load balancer distributes traffic between available servers.</p>
<p>Now we have:</p>
<pre><code class="language-text">Client
   ↓
Load Balancer
   ↓
┌────────┬────────┬────────┐
│Server 1│Server 2│Server 3│
└────────┴────────┴────────┘
</code></pre>
<p>This gives us more capacity and can improve availability.</p>
<p>But now we need to decide:</p>
<blockquote>
<p><strong>How does the load balancer choose a server?</strong></p>
</blockquote>
<hr />
<h2>Load-Balancing Algorithms</h2>
<p>There is not one universal algorithm.</p>
<p>Different algorithms work better for different workloads.</p>
<h3>Round Robin</h3>
<p>The load balancer sends requests in sequence:</p>
<pre><code class="language-text">Request 1 → Server 1
Request 2 → Server 2
Request 3 → Server 3
Request 4 → Server 1
</code></pre>
<p>Simple and useful when servers have similar capacity and requests have roughly similar cost.</p>
<h3>Weighted Round Robin</h3>
<p>Maybe our servers are not equally powerful.</p>
<pre><code class="language-text">Server 1 → weight 1
Server 2 → weight 2
Server 3 → weight 3
</code></pre>
<p>The stronger server receives more traffic.</p>
<h3>Least Connections</h3>
<p>The load balancer sends a new request to the server currently handling the fewest active connections.</p>
<pre><code class="language-text">Server 1 → 30 connections
Server 2 → 12 connections
Server 3 → 21 connections

New request → Server 2
</code></pre>
<p>This can be useful when requests have different durations.</p>
<h3>IP Hash</h3>
<p>The load balancer hashes the client's IP address and uses the result to choose a server.</p>
<p>Conceptually:</p>
<pre><code class="language-text">Client IP
   ↓
Hash
   ↓
Server selection
</code></pre>
<p>This can provide a form of request affinity, although relying on IP alone has limitations.</p>
<h3>Random</h3>
<p>The load balancer randomly chooses an available server.</p>
<p>Simple, but whether it is appropriate depends on the workload and implementation.</p>
<h3>Least Response Time</h3>
<p>The load balancer can consider response times and direct traffic toward servers that are responding faster.</p>
<p>This can be useful when server load is not evenly distributed.</p>
<h3>Consistent Hashing</h3>
<p><strong>Consistent hashing</strong> is useful in systems where we want related keys to map predictably to servers while minimizing how much mapping changes when servers are added or removed.</p>
<p>It becomes especially useful in distributed caches and partitioned systems.</p>
<p>Do not worry if that sounds complicated.</p>
<p>We will come back to it when we actually need it.</p>
<hr />
<h2>But What If a Server Is Dead?</h2>
<p>Imagine we have:</p>
<pre><code class="language-text">Server 1 → healthy
Server 2 → healthy
Server 3 → crashed
</code></pre>
<p>We do not want the load balancer to continue sending traffic to Server 3.</p>
<p>This is where <strong>health checks</strong> come in.</p>
<p>The load balancer periodically checks whether a server is healthy.</p>
<p>For example:</p>
<pre><code class="language-http">GET /health
</code></pre>
<p>The server might respond:</p>
<pre><code class="language-json">{
  "status": "ok"
}
</code></pre>
<p>If the server stops responding or reports that it is not ready, the load balancer can stop sending new traffic to it.</p>
<p>So now:</p>
<pre><code class="language-text">              Load Balancer
               /         \
              ↓           ↓
         Server 1      Server 2
         healthy       healthy

         Server 3
        unhealthy
            X
</code></pre>
<p>This is much better than blindly trusting that every server is alive.</p>
<p>If you want a ready-made health check implementation you can drop into an Express, Fastify or Hono app, I built one for Blockend. It handles liveness and readiness checks out of the box:</p>
<p><a href="https://blockend.noorulhassan.com/docs/02-blocks/07-health-check">https://blockend.noorulhassan.com/docs/02-blocks/07-health-check</a></p>
<hr />
<h2>Health Checks</h2>
<p>A load balancer should not blindly send traffic to every server.</p>
<p>Instead, it should periodically check whether each server is healthy.</p>
<p>For example:</p>
<pre><code class="language-text">Load Balancer
       /         \
      ↓           ↓
 Server 1      Server 2
 healthy       healthy

 Server 3
unhealthy
    X
</code></pre>
<p>This is much better than blindly trusting that every server is alive.</p>
<p>If you want a ready-made health check implementation you can drop into an Express, Fastify, or Hono app, I built one for Blockend. It handles liveness and readiness checks out of the box:</p>
<p><a href="https://blockend.noorulhassan.com/docs/02-blocks/07-health-check">https://blockend.noorulhassan.com/docs/02-blocks/07-health-check</a></p>
<h2>Health Checks Are More Than "Is the Process Running?"</h2>
<p>There is an important distinction here.</p>
<p>A server process can be running while the application is not actually ready to serve traffic.</p>
<p>For example:</p>
<pre><code class="language-text">Node process → running
Database     → unavailable
</code></pre>
<p>The process itself is alive.</p>
<p>But the application might not be able to perform useful work.</p>
<p>This is why production systems often distinguish between two different concepts:</p>
<ul>
<li><p>Liveness</p>
</li>
<li><p>Readiness</p>
</li>
</ul>
<h3>Liveness</h3>
<p>Liveness asks something like:</p>
<pre><code class="language-text">Is the application process alive?
</code></pre>
<h3>Readiness</h3>
<p>Readiness asks:</p>
<pre><code class="language-text">Is the application ready to receive traffic?
</code></pre>
<p>This distinction becomes important when deploying applications, restarting services, and handling failures.</p>
<h2>Redundancy</h2>
<p>We have now introduced another important idea:</p>
<p><strong>Redundancy.</strong></p>
<p>Instead of depending on one component:</p>
<pre><code class="language-text">Server
</code></pre>
<p>we have:</p>
<pre><code class="language-text">Server 1
Server 2
Server 3
</code></pre>
<p>If one fails, the others can continue serving traffic.</p>
<p>The same principle can apply to other parts of the system.</p>
<p>For example:</p>
<ul>
<li><p>Multiple application servers</p>
</li>
<li><p>Multiple database replicas</p>
</li>
<li><p>Multiple cache nodes</p>
</li>
<li><p>Multiple availability zones</p>
</li>
</ul>
<p>The goal is to avoid unnecessary single points of failure.</p>
<p>But redundancy creates another problem.</p>
<p>If we have three application servers, where is the user's session stored?</p>
<p>And if all three servers need the same data, how do they share it?</p>
<p>This is where our simple architecture starts becoming a distributed system.</p>
<h2>The Architecture We Have Built</h2>
<p>Let us stop here and look at how far we have come.</p>
<p>We started with:</p>
<pre><code class="language-text">Client → Server → Database
</code></pre>
<p>Then traffic increased.</p>
<p>We added vertical scaling.</p>
<p>Then we reached the limits of one machine.</p>
<p>We added horizontal scaling.</p>
<p>Then we needed something to distribute traffic.</p>
<p>We added a load balancer.</p>
<p>Then we needed to detect failed servers.</p>
<p>We added health checks.</p>
<p>Our architecture now looks roughly like:</p>
<pre><code class="language-text">                            ┌───────────┐
                            │ Database  │
                            └─────▲─────┘
                                  │
                                  │
                            ┌─────┴─────┐
                            │   Load    │
Client ─────────────────→ │ Balancer  │
                            └─────┬─────┘
                            ┌─────┼─────┐
                            ↓     ↓     ↓
                        Server Server Server
                           1     2     3
</code></pre>
<p>Notice something important.</p>
<p>We did not start with all of these components.</p>
<p>We earned each component by encountering a problem.</p>
<p>That is how I think system design should be learned.</p>
<h2>The Problems We Have Not Solved Yet</h2>
<p>Our system is better, but we are not finished.</p>
<p>We still have several problems.</p>
<h3>Database Bottleneck</h3>
<p>All servers are talking to one database.</p>
<p>What happens when database traffic becomes too high?</p>
<p>We might need:</p>
<ul>
<li><p>Indexes</p>
</li>
<li><p>Query optimization</p>
</li>
<li><p>Connection pooling</p>
</li>
<li><p>Caching</p>
</li>
<li><p>Read replicas</p>
</li>
<li><p>Partitioning</p>
</li>
<li><p>Sharding</p>
</li>
</ul>
<p>But we should not add these just because they exist.</p>
<p>We add them when the problem requires them.</p>
<h3>Repeated Expensive Requests</h3>
<p>Suppose thousands of users request the same product:</p>
<pre><code class="language-http">GET /products/123
</code></pre>
<p>Every request goes:</p>
<pre><code class="language-text">Server → Database
</code></pre>
<p>Why query the database 10,000 times if the data does not change frequently?</p>
<p>This is where caching becomes useful.</p>
<h3>Background Work</h3>
<p>Suppose creating an account also requires:</p>
<ul>
<li><p>Send email</p>
</li>
<li><p>Generate report</p>
</li>
<li><p>Process image</p>
</li>
<li><p>Notify another service</p>
</li>
</ul>
<p>Do we really want the HTTP request to wait for all of that?</p>
<p>Probably not.</p>
<p>This leads us toward:</p>
<ul>
<li><p>Queues</p>
</li>
<li><p>Background workers</p>
</li>
<li><p>Asynchronous processing</p>
</li>
</ul>
<h3>Service Failures</h3>
<p>What happens if another service is temporarily unavailable?</p>
<p>Do we retry?</p>
<p>How many times?</p>
<p>How quickly?</p>
<p>What if retries make the problem worse?</p>
<p>This leads us toward:</p>
<ul>
<li><p>Timeouts</p>
</li>
<li><p>Retries</p>
</li>
<li><p>Backoff</p>
</li>
<li><p>Circuit breakers</p>
</li>
<li><p>Idempotency</p>
</li>
</ul>
<h3>Data Growth</h3>
<p>What happens when our database grows from:</p>
<pre><code class="language-text">1 GB
</code></pre>
<p>to:</p>
<pre><code class="language-text">1 TB
</code></pre>
<p>Eventually we may need to think about:</p>
<ul>
<li><p>Partitioning</p>
</li>
<li><p>Archiving</p>
</li>
<li><p>Replication</p>
</li>
<li><p>Sharding</p>
</li>
</ul>
<p>And this is where system design starts becoming much deeper.</p>
<h2>The Mental Model I Want You to Keep</h2>
<p>If you are learning system design, do not try to memorize diagrams.</p>
<p>Instead, keep asking:</p>
<pre><code class="language-text">What is the bottleneck?
</code></pre>
<p>Then:</p>
<pre><code class="language-text">What failure can happen here?
</code></pre>
<p>Then:</p>
<pre><code class="language-text">What happens when traffic increases?
</code></pre>
<p>Then:</p>
<pre><code class="language-text">What happens when this component goes down?
</code></pre>
<p>Then:</p>
<pre><code class="language-text">What consistency does the application actually need?
</code></pre>
<p>Then:</p>
<pre><code class="language-text">What is the simplest solution that solves the problem?
</code></pre>
<p>That last question is important.</p>
<p>A system with:</p>
<pre><code class="language-text">Client
  ↓
Server
  ↓
PostgreSQL
</code></pre>
<p>is not a bad architecture simply because it does not have Kafka, Redis, Kubernetes, and 20 microservices.</p>
<p>If that system handles the workload reliably, it is a good system.</p>
<p>Good system design is not about adding more components.</p>
<p>It is about making the right tradeoffs for the problem you actually have.</p>
<h2>Where We Go From Here</h2>
<p>We started with one request:</p>
<pre><code class="language-text">Client
  ↓
Server
  ↓
Database
</code></pre>
<p>Then we introduced concepts only when we needed them:</p>
<pre><code class="language-text">More traffic
     ↓
Scaling
     ↓
Horizontal scaling
     ↓
Load balancer
     ↓
Health checks
     ↓
Redundancy
</code></pre>
<p>And this is only the beginning.</p>
<p>From here, we can start introducing:</p>
<pre><code class="language-text">Caching
   ↓
Database scaling
   ↓
Read replicas
   ↓
Queues
   ↓
Background workers
   ↓
Retries and backoff
   ↓
Idempotency
   ↓
Consistency
   ↓
Replication
   ↓
Partitioning
   ↓
Sharding
   ↓
Distributed systems
</code></pre>
<p>We will introduce each one when there is a real problem that requires it.</p>
<p>Because that is the way I wish someone had taught me system design.</p>
<p>Not:</p>
<blockquote>
<p>"Here are 50 components. Memorize them."</p>
</blockquote>
<p>But:</p>
<blockquote>
<p><strong>"Here is a system. Let's see what breaks when we push it."</strong></p>
</blockquote>
<p>That is where system design actually starts.</p>
<hr />
<h2>What You Learned</h2>
<p>Let us take a step back.</p>
<p>You started with a single browser request and ended up with:</p>
<ul>
<li><p>A server that processes requests</p>
</li>
<li><p>A relational database that stores structured data</p>
</li>
<li><p>Transactions that keep related operations consistent</p>
</li>
<li><p>Vertical scaling to handle more load</p>
</li>
<li><p>Horizontal scaling to handle even more</p>
</li>
<li><p>A load balancer that distributes traffic across servers</p>
</li>
<li><p>Health checks that detect failed servers</p>
</li>
<li><p>Redundancy that avoids single points of failure</p>
</li>
</ul>
<p>And you did not just memorize these concepts.</p>
<p>You <strong>encountered each problem</strong> and solved it with the right tool.</p>
<p>That is the mental model. That is what matters.</p>
<p>The next time someone says "you need a load balancer," you will not just know what it is.</p>
<p>You will know <strong>why</strong> it exists.</p>
<p>And that makes all the difference.</p>
]]></content:encoded></item><item><title><![CDATA[How to Prevent Users from Overwriting Each Other's Data: Optimistic Locking in Next.js & Prisma]]></title><description><![CDATA[Imagine you're building a collaborative business dashboard. Two project managers are reviewing the same contract at the same time. The contract currently has a budget of $4,000.
Manager A updates the ]]></description><link>https://engineeringdeepdives.hashnode.dev/how-to-prevent-users-from-overwriting-each-other-s-data-optimistic-locking-in-next-js-prisma</link><guid isPermaLink="true">https://engineeringdeepdives.hashnode.dev/how-to-prevent-users-from-overwriting-each-other-s-data-optimistic-locking-in-next-js-prisma</guid><category><![CDATA[Databases]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[optimistic locking]]></category><category><![CDATA[production-ready]]></category><category><![CDATA[prisma]]></category><dc:creator><![CDATA[Noor ul Hassan]]></dc:creator><pubDate>Fri, 10 Jul 2026 08:22:46 GMT</pubDate><content:encoded><![CDATA[<p>Imagine you're building a collaborative business dashboard. Two project managers are reviewing the same contract at the same time. The contract currently has a budget of <strong>$4,000</strong>.</p>
<p>Manager A updates the budget to <strong>$5,000</strong>.</p>
<p>Manager B updates the budget to <strong>$7,000</strong>.</p>
<p>Both click <strong>Save</strong> almost simultaneously.</p>
<p>In many CRUD applications, whichever request arrives last wins. If Manager B's request reaches the database a fraction of a second after Manager A's, the database blindly overwrites Manager A's changes. No warning. No error. No indication that data was lost.</p>
<p>This problem is called a <strong>race condition</strong>, and it's one of the easiest ways for multi-user systems to corrupt important business data.</p>
<p>I ran into this problem while designing a contract management workflow and realized that solving it isn't about writing smarter frontend code. The real solution lives where the data lives: the database.</p>
<p>Here's how optimistic locking works, why stateless servers can't solve this problem on their own, and how a single <code>version</code> column can protect your application from silent overwrites.</p>
<h2>Understanding the Problem</h2>
<p>Before discussing the fix, it's important to understand why this happens.</p>
<p>Modern Next.js applications often use Server Actions, Route Handlers, or API routes. These are <strong>stateless</strong>.</p>
<p>A stateless server treats every request as completely independent. When a user clicks Save, the server doesn't know:</p>
<ul>
<li><p>Who else is editing the record</p>
</li>
<li><p>Whether another request arrived milliseconds earlier</p>
</li>
<li><p>What version of the data the user originally saw</p>
</li>
</ul>
<p>The server simply receives a request and executes it.</p>
<p>This creates a race.</p>
<p>Suppose both users loaded the contract when it looked like this:</p>
<ul>
<li><p><strong>Budget:</strong> $4,000</p>
</li>
<li><p><strong>Version:</strong> 1</p>
</li>
</ul>
<p>Both users believe they're editing the latest version.</p>
<p>When they save, two independent requests race toward the database.</p>
<p>Without additional safeguards, whichever request finishes last overwrites the first one.</p>
<h2>The Traditional Update Problem</h2>
<p>Most applications initially implement updates like this:</p>
<pre><code class="language-typescript">await db.contract.update({
  where: {
    id: contractId,
  },
  data: {
    budget: newBudget,
  },
});
</code></pre>
<p>The database only checks whether the row exists.</p>
<p>It never asks:</p>
<ul>
<li><p>Is this data still current?</p>
</li>
<li><p>Has another user modified it?</p>
</li>
<li><p>Is this update based on stale information?</p>
</li>
</ul>
<p>As long as the record exists, the update succeeds.</p>
<p>That simplicity becomes dangerous in collaborative systems.</p>
<h2>The Solution: Optimistic Locking</h2>
<p>Optimistic locking assumes conflicts are uncommon.</p>
<p>Instead of locking rows and forcing users to wait, we allow everyone to read freely and only verify consistency when they attempt to write.</p>
<p>The technique relies on a simple concept:</p>
<p>Every record stores a version number.</p>
<pre><code class="language-typescript">model Contract {
  id           String   @id @default(uuid())
  contractName String
  budget       Int
  version      Int      @default(1)
  updatedAt    DateTime @updatedAt
}
</code></pre>
<p>Every successful update increments the version.</p>
<p>The version evolves like this:</p>
<p><code>Version 1 → Version 2 → Version 3 → Version 4 → ...</code></p>
<p>Whenever a user loads a record, they receive both the data and the current version number.</p>
<p>Later, when they submit changes, they must provide the version they originally saw.</p>
<h2>Implementing Optimistic Locking in Next.js</h2>
<p>The first step is fetching the contract and its version.</p>
<pre><code class="language-typescript">export async function getContract(id: string) {
  const contract = await db.contract.findUnique({
    where: { id },
  });

  return contract;
}
</code></pre>
<p>The frontend now stores both the contract data and the version number.</p>
<pre><code class="language-typescript">{
  budget: 4000,
  version: 1,
}
</code></pre>
<p>When the user saves changes, that version travels back to the server.</p>
<pre><code class="language-typescript">interface UpdateBudgetInput {
  contractId: string;
  newBudget: number;
  expectedVersion: number;
}
</code></pre>
<p>Now comes the important part.</p>
<p>Instead of updating by <code>id</code> alone, we update by both <code>id</code> and <code>version</code>.</p>
<pre><code class="language-typescript">const updateResult = await db.contract.updateMany({
  where: {
    id: contractId,
    version: expectedVersion,
  },
  data: {
    budget: newBudget,
    version: {
      increment: 1,
    },
  },
});
</code></pre>
<p>This query tells the database:</p>
<blockquote>
<p>Update the contract only if its version still matches the version the user originally loaded.</p>
</blockquote>
<p>If another user already updated the record, the versions no longer match.</p>
<p>The update affects zero rows.</p>
<pre><code class="language-typescript">if (updateResult.count === 0) {
  return {
    success: false,
    error: "VERSION_MISMATCH",
  };
}
</code></pre>
<p>And that's how the conflict is detected.</p>
<h2>Walking Through the Race Condition</h2>
<p>Let's replay the original scenario.</p>
<h3>Step 1: Both Users Load the Page</h3>
<p>The database returns:</p>
<ul>
<li><p><strong>Budget:</strong> $4,000</p>
</li>
<li><p><strong>Version:</strong> 1</p>
</li>
</ul>
<p>Manager A stores <code>expectedVersion = 1</code>.</p>
<p>Manager B stores <code>expectedVersion = 1</code>.</p>
<h3>Step 2: Both Click Save</h3>
<p>The requests begin racing toward the database.</p>
<h3>Step 3: Manager A Arrives First</h3>
<p>The database executes:</p>
<pre><code class="language-typescript">where: {
  id: contractId,
  version: 1,
}
</code></pre>
<p>A matching row exists.</p>
<p>The update succeeds.</p>
<p>The contract now contains:</p>
<ul>
<li><p><strong>Budget:</strong> $5,000</p>
</li>
<li><p><strong>Version:</strong> 2</p>
</li>
</ul>
<p>The database returns:</p>
<pre><code class="language-typescript">{
  count: 1;
}
</code></pre>
<p>Manager A sees a success message.</p>
<h3>Step 4: Manager B Arrives</h3>
<p>The database executes:</p>
<pre><code class="language-typescript">where: {
  id: contractId,
  version: 1,
}
</code></pre>
<p>But the record now has <strong>Version 2</strong>.</p>
<p>No row matches the condition.</p>
<p>The update affects zero records.</p>
<pre><code class="language-typescript">{
  count: 0;
}
</code></pre>
<p>The server immediately knows a conflict occurred.</p>
<p>Instead of overwriting Manager A's work, it returns:</p>
<pre><code class="language-typescript">{
  success: false,
  error: "VERSION_MISMATCH",
}
</code></pre>
<p>Manager B can refresh the page, review the latest changes, and decide how to proceed.</p>
<p>Most importantly, no data was lost.</p>
<h2>Why Use <code>updateMany()</code> Instead of <code>update()</code>?</h2>
<p>This implementation often surprises developers.</p>
<p>After all, we're updating a single row.</p>
<p>Why use <code>updateMany()</code>?</p>
<p>The answer is that <code>update()</code> It is designed around unique lookups.</p>
<p>Optimistic locking requires checking two conditions simultaneously:</p>
<pre><code class="language-typescript">{
  id: contractId,
  version: expectedVersion,
}
</code></pre>
<p><code>updateMany()</code> allows arbitrary filtering and returns a <code>count</code> value indicating how many records were modified.</p>
<p>That count becomes our conflict detector.</p>
<pre><code class="language-typescript">if (updateResult.count === 0) {
  // Version mismatch detected
}
</code></pre>
<p>Without that count, determining whether the update actually occurred becomes much harder.</p>
<h2>Why Optimistic Locking Scales Well</h2>
<p>Some systems use pessimistic locking instead.</p>
<p>A pessimistic lock effectively says:</p>
<blockquote>
<p>Someone is editing this record. Nobody else may modify it until they're finished.</p>
</blockquote>
<p>This guarantees consistency but creates bottlenecks and reduces concurrency.</p>
<p>Optimistic locking takes the opposite approach.</p>
<p>Everyone can read.</p>
<p>Everyone can edit.</p>
<p>The database only checks for conflicts when a write occurs.</p>
<p>Because conflicts are typically rare, this approach scales extremely well while still protecting data integrity.</p>
<p>That's why you'll find optimistic concurrency control in many SaaS products, enterprise applications, and collaborative platforms.</p>
<h2>Wrapping Up</h2>
<p>Race conditions aren't caused by bad databases or slow servers. They're a natural consequence of multiple users interacting with shared data simultaneously.</p>
<p>Because Next.js backends are stateless, the server cannot reliably determine who edited a record first. The database must become the source of truth.</p>
<p>By introducing a simple <code>version</code> column and validating it during updates, we transform a vulnerable CRUD application into a concurrency-aware system that actively protects business-critical data.</p>
<p>The implementation is surprisingly small:</p>
<ul>
<li><p>Add a version column.</p>
</li>
<li><p>Send the version to the client.</p>
</li>
<li><p>Verify the version during updates.</p>
</li>
<li><p>Increment the version after successful writes.</p>
</li>
</ul>
<p>That's it.</p>
<p>One integer column is often enough to prevent one of the most expensive classes of bugs in multi-user software: silent data overwrites.</p>
<p>Thanks for reading! Hope you enjoyed this post. If you have any questions or comments, feel free to reach out to me on <a href="https://www.linkedin.com/in/codewithnuh/">LinkedIn</a> or <a href="https://x.com/codewithnuh">X</a>.</p>
]]></content:encoded></item></channel></rss>