<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>System Design Lab Blog</title>
        <link>https://system-design-lab.pages.dev/blog</link>
        <description>Short, focused system design explainers.</description>
        <lastBuildDate>Mon, 10 Aug 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>Copyright © 2026 System Design Lab.</copyright>
        <item>
            <title><![CDATA[ACID Transactions Explained]]></title>
            <link>https://system-design-lab.pages.dev/blog/acid-transactions-explained</link>
            <guid>https://system-design-lab.pages.dev/blog/acid-transactions-explained</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Atomicity, Consistency, Isolation, Durability — what each ACID guarantee actually promises, and how isolation levels trade correctness for performance.]]></description>
            <content:encoded><![CDATA[<p>"ACID" gets thrown around as a synonym for "a real database," but each letter is a distinct, separately-breakable guarantee — and knowing which one a given failure violates is what actually matters in practice.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-four-letters">The four letters<a href="https://system-design-lab.pages.dev/blog/acid-transactions-explained#the-four-letters" class="hash-link" aria-label="Direct link to The four letters" title="Direct link to The four letters" translate="no">​</a></h2>
<ul>
<li class=""><strong>Atomicity</strong> — a transaction's operations happen as one indivisible unit: all of them succeed, or none of them do. A transfer that debits one account and credits another either does both or neither — never just one.</li>
<li class=""><strong>Consistency</strong> — a transaction moves the database from one valid state to another, respecting its own defined rules (constraints, foreign keys). Worth noting: this is a narrower guarantee than the "C" in the CAP theorem — related idea, different scope, and conflating the two is a common mistake.</li>
<li class=""><strong>Isolation</strong> — concurrent transactions don't see each other's uncommitted, in-progress changes. Without isolation, one transaction could read another's half-finished work.</li>
<li class=""><strong>Durability</strong> — once a transaction commits, it survives a crash. A confirmed write isn't going to vanish because the server lost power a second later.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="isolation-levels-how-much-concurrency-correctness-costs">Isolation levels: how much concurrency correctness costs<a href="https://system-design-lab.pages.dev/blog/acid-transactions-explained#isolation-levels-how-much-concurrency-correctness-costs" class="hash-link" aria-label="Direct link to Isolation levels: how much concurrency correctness costs" title="Direct link to Isolation levels: how much concurrency correctness costs" translate="no">​</a></h2>
<p>Isolation isn't all-or-nothing — databases offer a spectrum of isolation levels, each allowing a different set of anomalies in exchange for better concurrency performance:</p>
<table><thead><tr><th>Level</th><th>Allows</th><th>Typical default</th></tr></thead><tbody><tr><td>Read Uncommitted</td><td>Dirty reads (seeing another transaction's uncommitted changes)</td><td>Rarely used</td></tr><tr><td>Read Committed</td><td>Non-repeatable reads (a value changes between two reads in the same transaction)</td><td>PostgreSQL, SQL Server default</td></tr><tr><td>Repeatable Read</td><td>Phantom reads (a new row matching a query appears mid-transaction)</td><td>MySQL/InnoDB default</td></tr><tr><td>Serializable</td><td>Nothing — behaves as if transactions ran one at a time</td><td>Strongest, most expensive</td></tr></tbody></table>
<p>The pattern across the table: each stricter level closes off one more category of anomaly, at the cost of more locking or more work resolving conflicts, which is why almost no database defaults to full Serializable — most workloads don't need it, and the concurrency cost isn't worth paying by default.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="acid-and-the-cap-theorem">ACID and the CAP theorem<a href="https://system-design-lab.pages.dev/blog/acid-transactions-explained#acid-and-the-cap-theorem" class="hash-link" aria-label="Direct link to ACID and the CAP theorem" title="Direct link to ACID and the CAP theorem" translate="no">​</a></h2>
<p>ACID's guarantees, and strict isolation in particular, sit naturally on the CP side of the <a class="" href="https://system-design-lab.pages.dev/blog/cap-theorem-explained">CAP theorem</a>: serving a genuinely isolated, consistent view of the data is fundamentally in tension with staying available when replicas can't confirm they agree. This is why sharding and replication complicate ACID in practice: a transaction that used to be trivially atomic and isolated on a single machine has to coordinate across multiple machines once the data it touches is split across shards — the same cost <a class="" href="https://system-design-lab.pages.dev/blog/database-sharding-explained">database sharding</a> names explicitly as the price of scaling out a relational database.</p>
<div class="frame_Uhhe"><div class="header_nh7O"><img src="https://system-design-lab.pages.dev/img/logo.svg" alt="" class="logo_tTla"><span class="wordmark_EJV3">System Design Lab</span></div><div class="body_tyrq"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/acid-transactions-explained#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>"The database is ACID-compliant" isn't a design decision on its own — it's a starting property of whatever relational database you picked. The stronger conversation is about isolation level: naming which anomalies a specific piece of the system can tolerate (a dashboard read that's occasionally a few seconds stale is usually fine at Read Committed) versus which parts genuinely need Serializable (a double-booking check on the last seat in a flight). That's also exactly the same reasoning <a class="" href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency">strong vs. eventual consistency</a> applies at the replication layer — different mechanism, same underlying question of what correctness a specific piece of data actually needs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/acid-transactions-explained#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson includes the sequence diagram in more detail and expands on each isolation level's specific anomalies:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/database-fundamentals/acid-transactions">Read the full ACID Transactions lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Databases</category>
        </item>
        <item>
            <title><![CDATA[What Is the CAP Theorem? A Simple Explanation]]></title>
            <link>https://system-design-lab.pages.dev/blog/cap-theorem-explained</link>
            <guid>https://system-design-lab.pages.dev/blog/cap-theorem-explained</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[During a network partition, a distributed system must choose consistency or availability — not both. Here's what that actually means in practice.]]></description>
            <content:encoded><![CDATA[<p>The CAP theorem gets summarized so often as "pick two of three" that the actual, useful part — what it forces you to decide, and when — gets lost. Here's the version that's actually useful in a system design interview.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-three-properties">The three properties<a href="https://system-design-lab.pages.dev/blog/cap-theorem-explained#the-three-properties" class="hash-link" aria-label="Direct link to The three properties" title="Direct link to The three properties" translate="no">​</a></h2>
<ul>
<li class=""><strong>Consistency (C)</strong> — every read gets the most recent write, or an error. All nodes see the same data at the same time.</li>
<li class=""><strong>Availability (A)</strong> — every request gets a non-error response, with no guarantee it's the most recent write.</li>
<li class=""><strong>Partition Tolerance (P)</strong> — the system keeps working even when network messages between nodes are dropped or delayed.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-part-everyone-skims-past-p-isnt-optional">The part everyone skims past: P isn't optional<a href="https://system-design-lab.pages.dev/blog/cap-theorem-explained#the-part-everyone-skims-past-p-isnt-optional" class="hash-link" aria-label="Direct link to The part everyone skims past: P isn't optional" title="Direct link to The part everyone skims past: P isn't optional" translate="no">​</a></h2>
<p>Partitions <em>will</em> happen — cables get cut, switches fail, packets get dropped. Any system that spans more than one machine has to handle that reality eventually. Which means <strong>P isn't really a choice</strong> you get to opt out of. The actual decision CAP forces on you is what happens <em>during</em> a partition: <strong>do you choose consistency, or do you choose availability?</strong></p>
<ul>
<li class="">Choose <strong>C</strong>: nodes that can't confirm they have the latest data return an error rather than risk serving something stale.</li>
<li class="">Choose <strong>A</strong>: every node keeps answering requests, even if some of them serve slightly outdated data.</li>
</ul>
<p>There's no third option where you keep both during an actual partition — that's the whole theorem.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-nuance-most-explanations-miss">The nuance most explanations miss<a href="https://system-design-lab.pages.dev/blog/cap-theorem-explained#the-nuance-most-explanations-miss" class="hash-link" aria-label="Direct link to The nuance most explanations miss" title="Direct link to The nuance most explanations miss" translate="no">​</a></h2>
<p>Most of the time, there's no partition happening. The tradeoff you're actually managing day to day is closer to <strong>latency vs. consistency</strong>: requiring every replica to confirm a write before acknowledging it (strong consistency) adds latency compared to acknowledging as soon as one node has it (eventual consistency). This is captured by the follow-up framing, <strong>PACELC</strong>: <em>if Partitioned, choose Availability or Consistency; Else, choose Latency or Consistency.</em></p>
<p>One more distinction worth being precise about: "consistency" in CAP is a narrower, stricter guarantee than the "C" in ACID database transactions. They're related, but conflating them is a common mistake.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/cap-theorem-explained#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>"We chose AP for availability" is a weak answer on its own. A stronger one names <em>what specifically</em> breaks during a partition, and why the application can tolerate it — e.g., a social media feed can serve slightly stale data during a partition (choose A), but a payments ledger generally can't (choose C). Naming the actual tradeoff, not just citing the theorem, is what shows real understanding.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/cap-theorem-explained#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson has a decision diagram, walks through why "CA" systems are mostly a red flag in interview answers, and covers the pros/cons of choosing AP over CP with concrete examples:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/core-concepts/cap-theorem">Read the full CAP Theorem lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Core Concepts</category>
        </item>
        <item>
            <title><![CDATA[The Circuit Breaker Pattern Explained]]></title>
            <link>https://system-design-lab.pages.dev/blog/circuit-breaker-pattern</link>
            <guid>https://system-design-lab.pages.dev/blog/circuit-breaker-pattern</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A circuit breaker stops calling a failing dependency once errors cross a threshold, failing fast instead of piling up timeouts. Covers its three states.]]></description>
            <content:encoded><![CDATA[<p>When a downstream service starts failing, the naive response — every caller just keeps retrying — makes things worse, not better. A circuit breaker is the pattern that stops the pileup.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-retrying-everything-makes-a-bad-situation-worse">Why retrying everything makes a bad situation worse<a href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern#why-retrying-everything-makes-a-bad-situation-worse" class="hash-link" aria-label="Direct link to Why retrying everything makes a bad situation worse" title="Direct link to Why retrying everything makes a bad situation worse" translate="no">​</a></h2>
<p>A struggling service gets hit with the same load (or more, from retries) exactly when it's least able to handle it, while every caller wastes time and resources waiting on calls that are very likely to fail anyway. A circuit breaker wraps calls to a dependency, watches the failure rate, and once failures cross a threshold, stops sending traffic to that dependency entirely for a while — failing fast instead of piling on.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-three-states">The three states<a href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern#the-three-states" class="hash-link" aria-label="Direct link to The three states" title="Direct link to The three states" translate="no">​</a></h2>
<p>The pattern borrows its name, and its state machine, directly from the electrical original:</p>
<div class="frame_Uhhe"><div class="header_nh7O"><img src="https://system-design-lab.pages.dev/img/logo.svg" alt="" class="logo_tTla"><span class="wordmark_EJV3">System Design Lab</span></div><div class="body_tyrq"></div></div>
<ul>
<li class=""><strong>Closed</strong> — the normal state. Calls flow through, and the breaker tracks the failure rate.</li>
<li class=""><strong>Open</strong> — once failures exceed a threshold, the breaker trips: for a cooldown period, it fails every call <em>immediately</em>, without even attempting to reach the struggling dependency. This is the core value — protecting the caller from wasted waiting, and the callee from added load on top of whatever's already wrong.</li>
<li class=""><strong>Half-open</strong> — after the cooldown, the breaker cautiously lets a small number of test calls through. If they succeed, it closes and resumes normal traffic; if they still fail, it reopens and waits another cooldown before trying again.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="failing-fast-is-the-entire-point">Failing fast is the entire point<a href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern#failing-fast-is-the-entire-point" class="hash-link" aria-label="Direct link to Failing fast is the entire point" title="Direct link to Failing fast is the entire point" translate="no">​</a></h2>
<p>Without a breaker, a caller waiting on a slow, failing dependency ties up its own resources — connections, threads, request-handling capacity — for however long that call takes to time out. If enough callers pile up waiting the same way, the <em>caller</em> can fail too, purely from resource exhaustion, even though its own logic was fine. This is exactly the cascading-failure scenario a circuit breaker exists to interrupt: by failing immediately once the breaker is open, a caller gets an instant, predictable failure and can fall back to a default response, a cached value, or a graceful error, far faster than waiting out a real timeout.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-it-lives-in-a-real-system">Where it lives in a real system<a href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern#where-it-lives-in-a-real-system" class="hash-link" aria-label="Direct link to Where it lives in a real system" title="Direct link to Where it lives in a real system" translate="no">​</a></h2>
<p>A circuit breaker naturally wraps any single outbound call to a dependency — a service-to-service call, a database query, a call to a third-party API. It's a natural fit for an API gateway to own, since every downstream call in a system tends to flow through it anyway, making it a convenient single place to track per-dependency failure rates without instrumenting every individual service that makes the call.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reacting-vs-preventing-circuit-breaker-vs-rate-limiting">Reacting vs. preventing: circuit breaker vs. rate limiting<a href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern#reacting-vs-preventing-circuit-breaker-vs-rate-limiting" class="hash-link" aria-label="Direct link to Reacting vs. preventing: circuit breaker vs. rate limiting" title="Direct link to Reacting vs. preventing: circuit breaker vs. rate limiting" translate="no">​</a></h2>
<p>Both are protective patterns that reject requests under stress, but they solve different problems. <a class="" href="https://system-design-lab.pages.dev/blog/rate-limiting-explained">Rate limiting</a> protects a service from being overwhelmed by <em>too much legitimate demand</em> — it's proactive, rejecting requests before they'd exceed a known-safe capacity, and doesn't require anything to actually be broken yet. A circuit breaker instead protects callers <em>from a dependency that's already failing</em>, reacting to observed failures rather than anticipating load. Many real systems use both together: rate limiting on the way in, circuit breakers on every outbound call to their own dependencies.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>Any design involving a call to another service or third-party dependency benefits from naming a circuit breaker as the answer to "what happens if that dependency is slow or down." Naming the three states and the half-open recovery step specifically — not just "we'll add a circuit breaker" — signals you understand the mechanism, not just the vocabulary.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson expands on tuning failure thresholds and cooldown periods for real dependencies:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/distributed-systems-and-microservices/circuit-breaker">Read the full Circuit Breaker lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Distributed Systems</category>
        </item>
        <item>
            <title><![CDATA[Database Sharding Explained]]></title>
            <link>https://system-design-lab.pages.dev/blog/database-sharding-explained</link>
            <guid>https://system-design-lab.pages.dev/blog/database-sharding-explained</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Sharding splits a database across multiple machines so no single one holds all the data. Covers range-based vs. hash-based sharding and choosing a shard key.]]></description>
            <content:encoded><![CDATA[<p>A single database server has a hard ceiling — one machine's disk, memory, and CPU. Sharding is how you break past it: split the data across many independent databases instead of scaling one bigger and bigger.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-a-shard-actually-is">What a shard actually is<a href="https://system-design-lab.pages.dev/blog/database-sharding-explained#what-a-shard-actually-is" class="hash-link" aria-label="Direct link to What a shard actually is" title="Direct link to What a shard actually is" translate="no">​</a></h2>
<p>A shard is an independent, self-contained piece of a split-up database — its own instance, holding its own subset of the rows, capable of answering queries on its own without talking to any other shard for most operations. Which shard a given row lives on is decided by a <strong>shard key</strong>: a field on the row (often something like <code>user_id</code>) that gets run through a function to decide its home shard. Every query needs that shard key to know where to even look, which is the first cost sharding imposes: you can no longer just query "the database" — you have to know, or compute, where the data lives.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="range-based-sharding">Range-based sharding<a href="https://system-design-lab.pages.dev/blog/database-sharding-explained#range-based-sharding" class="hash-link" aria-label="Direct link to Range-based sharding" title="Direct link to Range-based sharding" translate="no">​</a></h2>
<p>The simplest approach: assign contiguous ranges of the shard key to each shard — user IDs 1–1,000,000 on shard A, 1,000,001–2,000,000 on shard B, and so on. It's easy to reason about and easy to implement, but it has an obvious failure mode: if activity isn't evenly distributed across the key range, some shards end up far hotter than others. A shard holding the newest user IDs in a fast-growing product, or the most active accounts in a range, can become a bottleneck while other shards sit mostly idle.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="hash-based-sharding">Hash-based sharding<a href="https://system-design-lab.pages.dev/blog/database-sharding-explained#hash-based-sharding" class="hash-link" aria-label="Direct link to Hash-based sharding" title="Direct link to Hash-based sharding" translate="no">​</a></h2>
<p>The alternative: run the shard key through a hash function and use the result to pick a shard. This spreads load far more evenly, since a good hash function doesn't preserve the kind of locality that creates hot spots. The cost is that range queries — "give me all users created this week" — become expensive, since consecutive keys are now scattered across every shard instead of sitting together. Hash-based sharding is typically paired with <strong>consistent hashing</strong>, which minimizes how much data has to move around when a shard is added or removed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-sharding-costs-you">What sharding costs you<a href="https://system-design-lab.pages.dev/blog/database-sharding-explained#what-sharding-costs-you" class="hash-link" aria-label="Direct link to What sharding costs you" title="Direct link to What sharding costs you" translate="no">​</a></h2>
<p>Splitting data across machines doesn't just add operational complexity — it changes what kinds of queries and transactions are even reasonably possible:</p>
<ul>
<li class=""><strong>Cross-shard queries and joins get expensive.</strong> A query that needs data from two different shards has to fan out to both and merge results in application code, instead of letting one database engine do it internally.</li>
<li class=""><strong>Cross-shard transactions lose easy ACID guarantees.</strong> A transaction touching rows on two different shards needs a distributed transaction protocol (like two-phase commit) to stay atomic — meaningfully more complex, and slower, than a transaction that stays on one machine.</li>
</ul>
<p>This is exactly why choosing a good shard key matters so much: a key that keeps related data — the rows a typical query actually needs together — on the same shard avoids paying this cost on the common path. <code>user_id</code> is a common choice specifically because most queries a product makes are scoped to one user anyway.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/database-sharding-explained#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>"We'll shard the database" is an incomplete answer on its own. A stronger one names the shard key and defends it (why this key, and why it keeps related data together), names the sharding strategy (range vs. hash, and the hot-spot tradeoff that choice implies), and is upfront about what gets harder as a result — cross-shard joins and transactions specifically. This is often where a <a class="" href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency">strong vs. eventual consistency</a> discussion follows naturally, since sharded systems frequently relax consistency to avoid the cost of coordinating across shards on every write.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/database-sharding-explained#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson includes a diagram of how a shard key routes a request to its shard, and walks through the range vs. hash tradeoff in more depth:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/database-fundamentals/database-sharding">Read the full Database Sharding lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Databases</category>
        </item>
        <item>
            <title><![CDATA[Horizontal vs Vertical Scaling Explained]]></title>
            <link>https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling</link>
            <guid>https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Vertical scaling means a bigger machine. Horizontal means more machines behind a load balancer. Here's how to actually decide, and why most systems do both.]]></description>
            <content:encoded><![CDATA[<p>When a system needs more capacity, there are exactly two ways to get it: make the machine you have bigger, or add more machines. Most real systems end up doing both — just not at the same time.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-quick-answer">The quick answer<a href="https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling#the-quick-answer" class="hash-link" aria-label="Direct link to The quick answer" title="Direct link to The quick answer" translate="no">​</a></h2>
<table><thead><tr><th></th><th>Vertical (scale up)</th><th>Horizontal (scale out)</th></tr></thead><tbody><tr><td>How</td><td>Add CPU/RAM/disk to one machine</td><td>Add more machines behind a load balancer</td></tr><tr><td>Ceiling</td><td>Hard limit — there's always a biggest machine</td><td>Effectively none</td></tr><tr><td>Architecture changes</td><td>None needed</td><td>Requires statelessness, a load balancer, a data-partitioning plan</td></tr><tr><td>Failure tolerance</td><td>Still one machine — no redundancy</td><td>One instance dying doesn't take the service down</td></tr><tr><td>Cost</td><td>Downtime to resize, and steep cost near the top</td><td>Incremental — cost scales with demand</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reach-for-vertical-scaling-when">Reach for vertical scaling when<a href="https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling#reach-for-vertical-scaling-when" class="hash-link" aria-label="Direct link to Reach for vertical scaling when" title="Direct link to Reach for vertical scaling when" translate="no">​</a></h2>
<ul>
<li class="">You need headroom fast and don't want to touch the architecture — it requires zero code changes.</li>
<li class="">The system is still small enough that "biggest available machine" is nowhere close to being a real ceiling.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reach-for-horizontal-scaling-when">Reach for horizontal scaling when<a href="https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling#reach-for-horizontal-scaling-when" class="hash-link" aria-label="Direct link to Reach for horizontal scaling when" title="Direct link to Reach for horizontal scaling when" translate="no">​</a></h2>
<ul>
<li class="">You've hit (or can see) the ceiling on vertical scaling, or downtime for resizing is no longer acceptable.</li>
<li class="">You need redundancy — a single machine, no matter how big, is still a single point of failure.</li>
<li class="">The workload can actually be split across machines — this is the real precondition people skip.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-horizontal-scaling-actually-costs">What horizontal scaling actually costs<a href="https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling#what-horizontal-scaling-actually-costs" class="hash-link" aria-label="Direct link to What horizontal scaling actually costs" title="Direct link to What horizontal scaling actually costs" translate="no">​</a></h2>
<p>It's not "more complex" in some vague sense — it's specific: every request now has to be routable to <em>any</em> instance, which means nothing about handling it can depend on state that only exists on one machine. A session, an in-memory cache, a WebSocket connection — each either needs to move to a shared store every instance can reach, or the system needs sticky routing, which reintroduces some of the coordination cost horizontal scaling was supposed to avoid. This is the same <a class="" href="https://system-design-lab.pages.dev/docs/system-design-tradeoffs/stateful-vs-stateless-design">stateful vs. stateless</a> tradeoff that shows up everywhere once you scale out.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-real-world-pattern-vertical-first-then-horizontal">The real-world pattern: vertical first, then horizontal<a href="https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling#the-real-world-pattern-vertical-first-then-horizontal" class="hash-link" aria-label="Direct link to The real-world pattern: vertical first, then horizontal" title="Direct link to The real-world pattern: vertical first, then horizontal" translate="no">​</a></h2>
<p>Almost no system picks one exclusively. The common, sensible order is: scale vertically first, because it's cheap and requires no architectural work, until the ceiling (or the downtime cost) starts to hurt — and only then invest in the real engineering work horizontal scaling requires. Databases follow this exact same progression.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>"We'll scale horizontally" as a reflex undersells the tradeoff. A stronger answer names what would actually need to change to make the workload in the prompt statelessly splittable — session storage, cache externalization, whatever it is — rather than assuming horizontal scaling is a free lever to pull.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson has a decision diagram and a precise breakdown of what makes a workload "splittable" in the first place:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/system-design-tradeoffs/vertical-vs-horizontal-scaling">Read the full Vertical vs Horizontal Scaling lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Tradeoffs</category>
        </item>
        <item>
            <title><![CDATA[How to Answer a System Design Interview Question]]></title>
            <link>https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question</link>
            <guid>https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A repeatable six-step framework for system design interviews: clarify requirements, estimate scale, design the API, model the data, sketch the architecture, then deep-dive.]]></description>
            <content:encoded><![CDATA[<p>"Design Twitter" is not a question with one correct answer — it's a prompt to see how you <em>think</em>. Walking in without a repeatable structure is the single most common reason strong engineers freeze up in this interview format.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-six-steps">The six steps<a href="https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question#the-six-steps" class="hash-link" aria-label="Direct link to The six steps" title="Direct link to The six steps" translate="no">​</a></h2>
<ol>
<li class=""><strong>Clarify requirements</strong> — what does this system actually need to do, and what's explicitly out of scope?</li>
<li class=""><strong>Estimate scale</strong> — rough numbers for users, requests per second, and data volume, since scale changes which architecture is even reasonable.</li>
<li class=""><strong>Design the API</strong> — the concrete contract between clients and your system, before you design what's behind it.</li>
<li class=""><strong>Model the data</strong> — what entities exist, and how are they shaped and related?</li>
<li class=""><strong>Sketch the high-level design</strong> — the boxes and arrows: services, databases, caches, queues, load balancers.</li>
<li class=""><strong>Deep-dive and discuss tradeoffs</strong> — pick the one or two hardest parts and go deep, naming what you gave up and why.</li>
</ol>
<p>Skipping straight to step 5 — drawing boxes before you know what you're building for — is the most common failure mode. It produces a generic-looking architecture that doesn't actually fit the problem, because nothing about it was derived from a real requirement or a real number.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-requirements-come-first">Why requirements come first<a href="https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question#why-requirements-come-first" class="hash-link" aria-label="Direct link to Why requirements come first" title="Direct link to Why requirements come first" translate="no">​</a></h2>
<p>"Design a URL shortener" sounds fully specified, but it isn't. Does it need custom aliases? Analytics on click-through? Do links expire? Every one of those answers changes the design. Asking clarifying questions isn't stalling — it's the only way to design <em>for</em> the actual problem instead of an imagined generic version of it. It also directly shapes step 2: you can't estimate scale for a system whose scope you haven't pinned down.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-scale-estimation-isnt-just-for-show">Why scale estimation isn't just for show<a href="https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question#why-scale-estimation-isnt-just-for-show" class="hash-link" aria-label="Direct link to Why scale estimation isn't just for show" title="Direct link to Why scale estimation isn't just for show" translate="no">​</a></h2>
<p>Rough numbers — "let's say 100 million daily active users, 10,000 writes per second at peak" — aren't decoration. They're what tells you whether a single relational database is fine or whether you need to talk about sharding at all. A design that reaches for a distributed, sharded architecture to handle 50 requests per second is over-engineered; a design that proposes a single Postgres instance for a system doing 500,000 writes per second is under-engineered. The numbers are what let you defend <em>why</em> your design is sized the way it is, rather than defaulting to whatever's trendiest.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="api-before-architecture">API before architecture<a href="https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question#api-before-architecture" class="hash-link" aria-label="Direct link to API before architecture" title="Direct link to API before architecture" translate="no">​</a></h2>
<p>Designing the API — the actual request/response shapes a client would call — before drawing any boxes forces you to be concrete about what the system does, from the outside, before you decide how it does it internally. It's also just good API design discipline: get the contract right first, and the implementation has a clear target to satisfy.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-deep-dive-is-where-interviews-are-actually-won-or-lost">The deep-dive is where interviews are actually won or lost<a href="https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question#the-deep-dive-is-where-interviews-are-actually-won-or-lost" class="hash-link" aria-label="Direct link to The deep-dive is where interviews are actually won or lost" title="Direct link to The deep-dive is where interviews are actually won or lost" translate="no">​</a></h2>
<p>The first five steps get you to a reasonable, defensible baseline design. The deep-dive is where you show real engineering judgment: picking the one or two genuinely hard parts of the problem — how do we handle a hot shard, how do we keep the feed consistent under concurrent writes, how do we deduplicate at-least-once message delivery — and reasoning through them out loud, including the tradeoffs you're accepting. This is also where topics like <a class="" href="https://system-design-lab.pages.dev/blog/database-sharding-explained">database sharding</a>, <a class="" href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency">strong vs. eventual consistency</a>, and <a class="" href="https://system-design-lab.pages.dev/blog/rate-limiting-explained">rate limiting</a> tend to show up as the specific mechanism you reach for.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="whats-actually-being-evaluated">What's actually being evaluated<a href="https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question#whats-actually-being-evaluated" class="hash-link" aria-label="Direct link to What's actually being evaluated" title="Direct link to What's actually being evaluated" translate="no">​</a></h2>
<p>There is no single "correct" architecture for "design Twitter." Interviewers are grading how you navigate tradeoffs, not whether you land on some canonical diagram. A candidate who clearly names <em>why</em> they chose eventual consistency for a like counter, or <em>why</em> they'd shard by user ID instead of by post ID, is demonstrating exactly the skill the interview exists to test — even if a different, equally defensible design was possible.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/how-to-answer-a-system-design-interview-question#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson walks through each step in more detail, includes a flowchart of the framework, and links out to the specific concepts — <a class="" href="https://system-design-lab.pages.dev/docs/api-fundamentals/apis">APIs</a>, <a class="" href="https://system-design-lab.pages.dev/blog/sql-vs-nosql">SQL vs. NoSQL</a>, <a class="" href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7">load balancing</a> — that tend to come up inside each step:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/interview-practice/how-to-answer-a-system-design-interview-question">Read the full How to Answer a System Design Interview Question lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Interview Practice</category>
        </item>
        <item>
            <title><![CDATA[Load Balancing Explained: Layer 4 vs Layer 7]]></title>
            <link>https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7</link>
            <guid>https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[L4 load balancers route on IP and port alone. L7 load balancers read the actual request — and that's not just for HTTP. Here's the real difference.]]></description>
            <content:encoded><![CDATA[<p>A load balancer distributes requests across servers so no single one gets overwhelmed. The interesting decision isn't "should I use one" — it's which layer of the <a class="" href="https://system-design-lab.pages.dev/docs/networking-fundamentals/osi-model">OSI Model</a> it operates at, because that choice has real consequences for speed, cost, and what it can actually do.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-quick-answer">The quick answer<a href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7#the-quick-answer" class="hash-link" aria-label="Direct link to The quick answer" title="Direct link to The quick answer" translate="no">​</a></h2>
<table><thead><tr><th></th><th>Layer 4 (transport)</th><th>Layer 7 (application)</th></tr></thead><tbody><tr><td>Decides based on</td><td>IP address and port only</td><td>The actual request content</td></tr><tr><td>Speed</td><td>Fast — just forwards packets</td><td>Slower — has to parse the protocol</td></tr><tr><td>Protocol awareness</td><td>None needed</td><td>Has to understand the specific protocol</td></tr><tr><td>Can do</td><td>Simple, protocol-agnostic forwarding</td><td>Content-aware routing, sticky sessions, request inspection</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="layer-4-fast-and-protocol-agnostic">Layer 4: fast and protocol-agnostic<a href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7#layer-4-fast-and-protocol-agnostic" class="hash-link" aria-label="Direct link to Layer 4: fast and protocol-agnostic" title="Direct link to Layer 4: fast and protocol-agnostic" translate="no">​</a></h2>
<p>An L4 load balancer routes based on connection info alone — it doesn't need to know or care that the traffic is HTTP, or anything else. That makes it fast and simple, at the cost of not being able to make any decision based on what's actually inside the request.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="layer-7-reads-the-request-pays-for-it">Layer 7: reads the request, pays for it<a href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7#layer-7-reads-the-request-pays-for-it" class="hash-link" aria-label="Direct link to Layer 7: reads the request, pays for it" title="Direct link to Layer 7: reads the request, pays for it" translate="no">​</a></h2>
<p>An L7 load balancer terminates the connection and inspects the actual request before deciding where it goes. For HTTP, that means URL paths, headers, and cookies — enabling content-aware routing (<code>/api/*</code> to one pool, <code>/static/*</code> to another) and sticky sessions. The cost is real: more CPU work and higher latency per request, since the proxy has to actually parse the application-layer protocol.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="l7-isnt-only-about-http">L7 isn't only about HTTP<a href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7#l7-isnt-only-about-http" class="hash-link" aria-label="Direct link to L7 isn't only about HTTP" title="Direct link to L7 isn't only about HTTP" translate="no">​</a></h2>
<p>This is the part most explanations leave out. "Application layer" means whatever protocol is actually running — and a load balancer built to parse that protocol can make the same kind of smart decision on it:</p>
<ul>
<li class=""><strong>Databases</strong> — tools like ProxySQL (MySQL) and pgpool-II (PostgreSQL) read the query itself, routing <code>SELECT</code>s to read replicas and writes to the primary.</li>
<li class=""><strong>Caches</strong> — protocol-aware proxies like Twemproxy understand Redis/Memcached well enough to shard keys consistently across a cluster.</li>
<li class=""><strong>VoIP</strong> — SIP-aware proxies route calls by parsing the actual signaling protocol, not just IP and port.</li>
</ul>
<p>Same tradeoff every time: understand more of the traffic, make a smarter routing decision, pay more CPU per request for it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-it-picks-which-server">How it picks <em>which</em> server<a href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7#how-it-picks-which-server" class="hash-link" aria-label="Direct link to how-it-picks-which-server" title="Direct link to how-it-picks-which-server" translate="no">​</a></h2>
<p>Once a load balancer decides it's routing to a pool, it still needs a policy for which one — round robin, least connections, or consistent hashing (so the same client keeps hitting the same server, keeping that server's local cache warm). The algorithm matters as much as the layer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>Naming the layer <em>and</em> the algorithm — "an L7 load balancer with least-connections, since request durations vary a lot here" — is a specific engineering decision, not a generic "we'll add a load balancer." That specificity is what separates a real answer from a diagram with a box labeled "LB."</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson has a health-check diagram, the full routing-algorithm comparison table, and diagrams for the database and cache examples above:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/networking-fundamentals/load-balancing">Read the full Load Balancing lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Networking</category>
        </item>
        <item>
            <title><![CDATA[Rate Limiting Explained: Token Bucket and Beyond]]></title>
            <link>https://system-design-lab.pages.dev/blog/rate-limiting-explained</link>
            <guid>https://system-design-lab.pages.dev/blog/rate-limiting-explained</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Rate limiting caps how many requests a client can make and rejects the rest. A breakdown of fixed window, sliding window, token bucket, and leaky bucket.]]></description>
            <content:encoded><![CDATA[<p>Rate limiting is a deliberately blunt tool: instead of trying to make every request cheap, it accepts that some requests get turned away, so the system stays up for everyone else.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="not-the-same-job-as-load-balancing">Not the same job as load balancing<a href="https://system-design-lab.pages.dev/blog/rate-limiting-explained#not-the-same-job-as-load-balancing" class="hash-link" aria-label="Direct link to Not the same job as load balancing" title="Direct link to Not the same job as load balancing" translate="no">​</a></h2>
<p>It's worth being precise about the difference: <a class="" href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7">load balancing</a> assumes the incoming traffic is legitimate and spreads it out efficiently. Rate limiting assumes some incoming traffic might be excessive or abusive, and its job is to say no to some of it — typically with an HTTP <code>429 Too Many Requests</code> — before it ever reaches real business logic.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-a-service-needs-this-at-all">Why a service needs this at all<a href="https://system-design-lab.pages.dev/blog/rate-limiting-explained#why-a-service-needs-this-at-all" class="hash-link" aria-label="Direct link to Why a service needs this at all" title="Direct link to Why a service needs this at all" translate="no">​</a></h2>
<p>Without a limit, one misbehaving client — a buggy retry loop, a scraper, a deliberate abuser — can consume a disproportionate share of a service's capacity, degrading it for everyone else. It's the standard first line of defense against basic denial-of-service traffic, and a practical necessity for metering usage on any API with paid tiers. Rate limiting is typically enforced once, centrally, at an API gateway — so individual backend services don't each have to reimplement the same logic, and a request that's going to be rejected anyway never costs the backend any real work.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-algorithms">The algorithms<a href="https://system-design-lab.pages.dev/blog/rate-limiting-explained#the-algorithms" class="hash-link" aria-label="Direct link to The algorithms" title="Direct link to The algorithms" translate="no">​</a></h2>
<table><thead><tr><th>Algorithm</th><th>How it works</th><th>Tradeoff</th></tr></thead><tbody><tr><td>Fixed window</td><td>Count requests in a fixed clock interval, reset each interval</td><td>Simple, but allows a burst of 2x the limit right at a window boundary</td></tr><tr><td>Sliding window</td><td>Count requests in a rolling window ending "now"</td><td>Smooths the boundary-burst problem, costs more to compute</td></tr><tr><td>Token bucket</td><td>A bucket refills at a steady rate; each request spends one token</td><td>Allows controlled bursts up to bucket size, enforces a true average rate</td></tr><tr><td>Leaky bucket</td><td>Requests queue and drain at a fixed steady rate</td><td>Smooths bursts into steady output, adds queueing latency</td></tr></tbody></table>
<div class="frame_Uhhe"><div class="header_nh7O"><img src="https://system-design-lab.pages.dev/img/logo.svg" alt="" class="logo_tTla"><span class="wordmark_EJV3">System Design Lab</span></div><div class="body_tyrq"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-token-bucket-is-the-go-to-answer">Why token bucket is the go-to answer<a href="https://system-design-lab.pages.dev/blog/rate-limiting-explained#why-token-bucket-is-the-go-to-answer" class="hash-link" aria-label="Direct link to Why token bucket is the go-to answer" title="Direct link to Why token bucket is the go-to answer" translate="no">​</a></h2>
<p>Token bucket is the most commonly cited algorithm in interviews because it fits how real traffic actually behaves: legitimate clients are often bursty — a page load firing off several requests at once — and token bucket tolerates that burst as long as the <em>average</em> rate stays within budget. Fixed window, by contrast, either lets through a burst it shouldn't (right at a window boundary) or blocks a burst that was actually fine.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-key-you-limit-on-matters-as-much-as-the-algorithm">What key you limit on matters as much as the algorithm<a href="https://system-design-lab.pages.dev/blog/rate-limiting-explained#what-key-you-limit-on-matters-as-much-as-the-algorithm" class="hash-link" aria-label="Direct link to What key you limit on matters as much as the algorithm" title="Direct link to What key you limit on matters as much as the algorithm" translate="no">​</a></h2>
<p>The algorithm decides <em>how</em> to count; the key decides <em>who's</em> being counted. Per-IP limiting can unfairly throttle many legitimate users sitting behind the same corporate NAT or proxy, while per-user or per-API-key limiting is more precise but only works once a client is authenticated — which is why public, unauthenticated endpoints often fall back to IP-based limiting as a coarser first line of defense.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tell-the-client-where-it-stands">Tell the client where it stands<a href="https://system-design-lab.pages.dev/blog/rate-limiting-explained#tell-the-client-where-it-stands" class="hash-link" aria-label="Direct link to Tell the client where it stands" title="Direct link to Tell the client where it stands" translate="no">​</a></h2>
<p>A well-designed rate-limited API doesn't just reject silently at the threshold — it signals via response headers like <code>X-RateLimit-Limit</code>, <code>X-RateLimit-Remaining</code>, and <code>X-RateLimit-Reset</code>, so a well-behaved client can back off proactively instead of hammering the API until it gets a <code>429</code>. It's a small detail, but naming it signals you're thinking about the client's experience of the limit, not just the server's enforcement of it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/rate-limiting-explained#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>"We'll add rate limiting" is weak on its own. Naming the algorithm (token bucket, for bursty-but-bounded traffic), where it's enforced (the gateway, so rejected requests cost the backend nothing), and what key it's keyed on turns it into a specific, defensible decision. It's also the natural complement to a <a class="" href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern">circuit breaker</a> — rate limiting protects a service from too much legitimate demand coming in, while a circuit breaker protects callers from a dependency that's already failing going out.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/rate-limiting-explained#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson expands on each algorithm's boundary behavior and where rate limiting fits inside an API gateway:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/api-fundamentals/rate-limiting">Read the full Rate Limiting lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>APIs</category>
        </item>
        <item>
            <title><![CDATA[REST vs GraphQL: Which API Style Should You Use?]]></title>
            <link>https://system-design-lab.pages.dev/blog/rest-vs-graphql</link>
            <guid>https://system-design-lab.pages.dev/blog/rest-vs-graphql</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[REST gives you free HTTP caching and simple tooling. GraphQL gives clients exactly the fields they ask for. Here's the tradeoff that actually decides it.]]></description>
            <content:encoded><![CDATA[<p>REST and GraphQL are two different answers to the same question: how should an API expose data to a client? The real decision isn't "which is more modern" — it's whether you're willing to give up free HTTP caching for a query language that fetches exactly what the client asks for.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-quick-answer">The quick answer<a href="https://system-design-lab.pages.dev/blog/rest-vs-graphql#the-quick-answer" class="hash-link" aria-label="Direct link to The quick answer" title="Direct link to The quick answer" translate="no">​</a></h2>
<table><thead><tr><th></th><th>REST</th><th>GraphQL</th></tr></thead><tbody><tr><td>Shape</td><td>Fixed per endpoint (<code>GET /users/42</code> always returns the same shape)</td><td>Client-specified per request</td></tr><tr><td>Endpoints</td><td>Many, one per resource</td><td>Usually one</td></tr><tr><td>Over/underfetching</td><td>Common — you get exactly what the endpoint defines, no more or less</td><td>Fixed by design — you ask for exactly the fields you need</td></tr><tr><td>HTTP caching</td><td>Free — a <code>GET</code> is cacheable by URL out of the box</td><td>Not out of the box — needs its own caching layer</td></tr><tr><td>Best for</td><td>Public APIs, CRUD resources, simple clients</td><td>Multiple clients needing very different shapes of the same data</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reach-for-rest-when">Reach for REST when<a href="https://system-design-lab.pages.dev/blog/rest-vs-graphql#reach-for-rest-when" class="hash-link" aria-label="Direct link to Reach for REST when" title="Direct link to Reach for REST when" translate="no">​</a></h2>
<ul>
<li class="">It's a public API, a CRUD-shaped resource, or anything that benefits from standard HTTP caching, versioning, and tooling that already assumes REST (most API gateways and monitoring do).</li>
<li class="">You have one kind of client and its data needs are fairly uniform.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reach-for-graphql-when">Reach for GraphQL when<a href="https://system-design-lab.pages.dev/blog/rest-vs-graphql#reach-for-graphql-when" class="hash-link" aria-label="Direct link to Reach for GraphQL when" title="Direct link to Reach for GraphQL when" translate="no">​</a></h2>
<ul>
<li class="">You have genuinely different clients wanting different slices of the same data — a web app, an iOS app, and a smartwatch app each needing their own shape is the textbook case.</li>
<li class="">The team can afford building the server-side resolver and batching layer GraphQL requires.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-tradeoff-most-comparisons-skip-caching">The tradeoff most comparisons skip: caching<a href="https://system-design-lab.pages.dev/blog/rest-vs-graphql#the-tradeoff-most-comparisons-skip-caching" class="hash-link" aria-label="Direct link to The tradeoff most comparisons skip: caching" title="Direct link to The tradeoff most comparisons skip: caching" translate="no">​</a></h2>
<p>This is the one that actually matters for a system design decision. REST's one-URL-per-resource model means a browser, CDN, or reverse proxy can cache a <code>GET /users/42</code> request with zero extra work — caching is a property of the protocol itself. GraphQL typically exposes a single endpoint reached via <code>POST</code>, which means none of that standard caching infrastructure applies automatically. A GraphQL server has to build its own caching (normalized client-side caches like Apollo, or persisted queries) — real engineering work that REST gets for free.</p>
<p>The other GraphQL-specific cost worth knowing: the <strong>N+1 problem</strong>. Naively resolving a nested query (a list of users, each resolving its own orders) fires one database query per item instead of one batched query for everyone — unless the server explicitly batches those resolutions.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-real-world-answer-often-both">The real-world answer: often both<a href="https://system-design-lab.pages.dev/blog/rest-vs-graphql#the-real-world-answer-often-both" class="hash-link" aria-label="Direct link to The real-world answer: often both" title="Direct link to The real-world answer: often both" translate="no">​</a></h2>
<p>Many production systems use REST at the edge for simple, cacheable resources, and GraphQL for one specific client-facing aggregation layer that genuinely needs to serve different shapes to different clients — not one or the other for the whole system.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/rest-vs-graphql#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson has a diagram comparing the request shapes directly, walks through the N+1 problem with a concrete example, and gives the interview framing for defending whichever choice you make:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/api-fundamentals/rest-vs-graphql">Read the full REST vs GraphQL lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>APIs</category>
        </item>
        <item>
            <title><![CDATA[SQL vs NoSQL: How to Choose the Right Database]]></title>
            <link>https://system-design-lab.pages.dev/blog/sql-vs-nosql</link>
            <guid>https://system-design-lab.pages.dev/blog/sql-vs-nosql</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A quick, practical breakdown of when to reach for SQL versus NoSQL — the real tradeoff, not just "NoSQL scales better."]]></description>
            <content:encoded><![CDATA[<p>"SQL or NoSQL?" sounds like a technology choice. It's actually a question about how much structure and consistency you're willing to trade for flexibility and scale — and the honest answer for most systems is "some of each," not one winner.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-quick-answer">The quick answer<a href="https://system-design-lab.pages.dev/blog/sql-vs-nosql#the-quick-answer" class="hash-link" aria-label="Direct link to The quick answer" title="Direct link to The quick answer" translate="no">​</a></h2>
<table><thead><tr><th></th><th>SQL (relational)</th><th>NoSQL</th></tr></thead><tbody><tr><td>Schema</td><td>Fixed, enforced by the database</td><td>Flexible or absent</td></tr><tr><td>Consistency</td><td>Strong (ACID transactions)</td><td>Often relaxed, favors availability</td></tr><tr><td>Relationships</td><td>Joins across normalized tables</td><td>Denormalized — related data duplicated together</td></tr><tr><td>Scales out by</td><td>Getting harder as joins/transactions cross machines</td><td>Sharding cleanly, since records rarely need each other</td></tr><tr><td>Best for</td><td>Correctness-critical, relationally complex data</td><td>High-scale, well-known access patterns</td></tr></tbody></table>
<p>Neither row is "better" — they're optimized for different problems.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reach-for-sql-when">Reach for SQL when<a href="https://system-design-lab.pages.dev/blog/sql-vs-nosql#reach-for-sql-when" class="hash-link" aria-label="Direct link to Reach for SQL when" title="Direct link to Reach for SQL when" translate="no">​</a></h2>
<ul>
<li class="">The data is naturally relational (orders, customers, inventory, payments) and you'll need to join across it.</li>
<li class="">Correctness matters more than raw throughput — a financial ledger can't tolerate "eventually consistent."</li>
<li class="">You don't fully know your query patterns yet. A fixed schema with joins gives you the flexibility to ask new questions later; a denormalized NoSQL store only answers the questions it was shaped for.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="reach-for-nosql-when">Reach for NoSQL when<a href="https://system-design-lab.pages.dev/blog/sql-vs-nosql#reach-for-nosql-when" class="hash-link" aria-label="Direct link to Reach for NoSQL when" title="Direct link to Reach for NoSQL when" translate="no">​</a></h2>
<ul>
<li class="">The data is naturally document- or key-value-shaped, and its structure varies or evolves quickly.</li>
<li class="">You already know your access patterns well enough to denormalize around them.</li>
<li class="">Horizontal scale or write throughput matters more than ad hoc query flexibility.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-nosql-usually-scales-out-more-easily">Why NoSQL usually scales out more easily<a href="https://system-design-lab.pages.dev/blog/sql-vs-nosql#why-nosql-usually-scales-out-more-easily" class="hash-link" aria-label="Direct link to Why NoSQL usually scales out more easily" title="Direct link to Why NoSQL usually scales out more easily" translate="no">​</a></h2>
<p>This is the part most comparisons skip: it's not that NoSQL databases are "faster" in some general sense. It's that avoiding cross-record joins and relaxing strict consistency means related data can live entirely within one shard, so shards rarely need to coordinate with each other. A relational database's join or transaction, by contrast, might need data sitting on a different machine entirely — which is expensive to coordinate at scale. Modern relational systems (distributed SQL like Spanner or CockroachDB) have narrowed this gap a lot, but the underlying tension is still there.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-real-world-answer-often-both">The real-world answer: often both<a href="https://system-design-lab.pages.dev/blog/sql-vs-nosql#the-real-world-answer-often-both" class="hash-link" aria-label="Direct link to The real-world answer: often both" title="Direct link to The real-world answer: often both" translate="no">​</a></h2>
<p>Most systems at any real scale use a relational database for their core transactional data, and a NoSQL store for one specific high-scale pattern — a product catalog, a session store, an activity feed — rather than picking one for the entire system.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/sql-vs-nosql#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>This post is the short version. The full lesson covers the CAP theorem tradeoff NoSQL databases are actually making, walks through <em>why</em> denormalization enables sharding with a diagram, and gives the interview framing for defending whichever choice you make:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/database-fundamentals/sql-vs-nosql">Read the full SQL vs NoSQL lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Databases</category>
        </item>
        <item>
            <title><![CDATA[Stateful vs Stateless Design Explained]]></title>
            <link>https://system-design-lab.pages.dev/blog/stateful-vs-stateless-design</link>
            <guid>https://system-design-lab.pages.dev/blog/stateful-vs-stateless-design</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A stateless server treats every request as self-sufficient and scales horizontally without sticky routing. A stateful one holds client context in memory.]]></description>
            <content:encoded><![CDATA[<p>Where does a server keep what it knows about a client between one request and the next? The answer to that one question decides how easily the whole system scales.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="stateless-every-request-carries-what-it-needs">Stateless: every request carries what it needs<a href="https://system-design-lab.pages.dev/blog/stateful-vs-stateless-design#stateless-every-request-carries-what-it-needs" class="hash-link" aria-label="Direct link to Stateless: every request carries what it needs" title="Direct link to Stateless: every request carries what it needs" translate="no">​</a></h2>
<p>A stateless server treats each incoming request as fully self-sufficient — everything needed to handle it (an auth token, the relevant IDs, the data itself) travels with the request, and the server keeps nothing about the client in its own memory afterward. This pairs naturally with <a class="" href="https://system-design-lab.pages.dev/blog/horizontal-vs-vertical-scaling">horizontal scaling</a>: because any stateless server instance can handle any request equally well, a load balancer can route traffic to whichever instance is least busy, with no need for "sticky" routing that pins a client to one specific server.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="stateful-the-server-remembers-you">Stateful: the server remembers you<a href="https://system-design-lab.pages.dev/blog/stateful-vs-stateless-design#stateful-the-server-remembers-you" class="hash-link" aria-label="Direct link to Stateful: the server remembers you" title="Direct link to Stateful: the server remembers you" translate="no">​</a></h2>
<p>A stateful server holds context about a specific client in its own memory between requests — a session object, an in-progress multi-step operation, an open connection. This makes certain interactions simpler to reason about, but it means a client generally has to keep talking to <em>that specific server instance</em> for the interaction to keep working, which is exactly the sticky-routing requirement statelessness avoids.</p>
<div class="frame_Uhhe"><div class="header_nh7O"><img src="https://system-design-lab.pages.dev/img/logo.svg" alt="" class="logo_tTla"><span class="wordmark_EJV3">System Design Lab</span></div><div class="body_tyrq"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="statelessness-doesnt-mean-state-disappears--it-means-it-moves">Statelessness doesn't mean state disappears — it means it moves<a href="https://system-design-lab.pages.dev/blog/stateful-vs-stateless-design#statelessness-doesnt-mean-state-disappears--it-means-it-moves" class="hash-link" aria-label="Direct link to Statelessness doesn't mean state disappears — it means it moves" title="Direct link to Statelessness doesn't mean state disappears — it means it moves" translate="no">​</a></h2>
<p>This is the part that trips people up: a stateless <em>architecture</em> doesn't mean the application has no state at all — a shopping cart, a user's session, a logged-in identity are all still real state that has to live somewhere. What changes is <em>where</em>. Instead of living in one server's process memory, it moves to a shared store every server instance can reach equally — a database, a distributed cache like Redis, or an encoded, signed token (a JWT) the client carries and presents on every request. Any server can then serve any request, because the state it needs isn't tied to which server happened to handle the client last.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-resistant-case-websockets">The resistant case: WebSockets<a href="https://system-design-lab.pages.dev/blog/stateful-vs-stateless-design#the-resistant-case-websockets" class="hash-link" aria-label="Direct link to The resistant case: WebSockets" title="Direct link to The resistant case: WebSockets" translate="no">​</a></h2>
<p>Not everything can be made stateless cleanly. A WebSocket connection is a genuine counterexample: the open, persistent connection between a specific client and a specific server <em>is</em> the state — there's no way to externalize "this TCP connection is open" to a shared database the way you can externalize a session token. Real-time systems built on WebSockets typically need sticky routing (or a connection-aware layer that can route messages to whichever server actually holds the relevant socket) precisely because this one piece of state can't be moved out of the server holding the connection.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/stateful-vs-stateless-design#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>Defaulting to stateless design is usually the right instinct for anything expected to scale horizontally — it's what makes adding more servers behind a load balancer actually work without extra coordination. But naming <em>where</em> the externalized state goes (which store, and why it's fast enough not to become the new bottleneck) is what separates a real answer from just saying "make it stateless." And naming WebSockets — or any other case where state genuinely can't move — as the exception shows you understand the tradeoff isn't universal, just usually correct.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/stateful-vs-stateless-design#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson goes deeper on session storage options and where sticky routing legitimately earns its place:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/system-design-tradeoffs/stateful-vs-stateless-design">Read the full Stateful vs Stateless Design lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Tradeoffs</category>
        </item>
        <item>
            <title><![CDATA[Strong vs Eventual Consistency Explained]]></title>
            <link>https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency</link>
            <guid>https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Strong consistency guarantees every read sees the latest write, at a real latency cost. Eventual consistency acks fast and lets replicas catch up later.]]></description>
            <content:encoded><![CDATA[<p>Every write to a replicated system eventually has to answer one question: does the caller wait for every copy to agree before we call it done, or do we acknowledge fast and let the copies catch up on their own time?</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="strong-consistency-every-read-sees-the-latest-write">Strong consistency: every read sees the latest write<a href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency#strong-consistency-every-read-sees-the-latest-write" class="hash-link" aria-label="Direct link to Strong consistency: every read sees the latest write" title="Direct link to Strong consistency: every read sees the latest write" translate="no">​</a></h2>
<p>Under strong consistency, a write isn't acknowledged until enough replicas confirm they have it, and a read afterward is guaranteed to reflect it — there's no window where a client could read stale data right after a confirmed write. This is the C side of the <a class="" href="https://system-design-lab.pages.dev/blog/cap-theorem-explained">CAP theorem</a>: strong consistency is what a CP system chooses to preserve, typically at the cost of added write latency (waiting on replicas) or reduced availability (if enough replicas can't be reached, the write has to fail rather than risk an inconsistent read later).</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="eventual-consistency-fast-ack-catch-up-later">Eventual consistency: fast ack, catch up later<a href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency#eventual-consistency-fast-ack-catch-up-later" class="hash-link" aria-label="Direct link to Eventual consistency: fast ack, catch up later" title="Direct link to Eventual consistency: fast ack, catch up later" translate="no">​</a></h2>
<p>Under eventual consistency, a write is acknowledged as soon as it lands somewhere — often just one node — and the update propagates to other replicas afterward, typically via mechanisms like Change Data Capture. In the meantime, a read against a replica that hasn't caught up yet can return stale data. What "eventual" actually promises is narrower than it sounds: if no new writes happen, all replicas <em>will</em> converge to the same value — it just doesn't say when.</p>
<div class="frame_Uhhe"><div class="header_nh7O"><img src="https://system-design-lab.pages.dev/img/logo.svg" alt="" class="logo_tTla"><span class="wordmark_EJV3">System Design Lab</span></div><div class="body_tyrq"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-weaker-guarantees-are-chosen-on-purpose">Where weaker guarantees are chosen on purpose<a href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency#where-weaker-guarantees-are-chosen-on-purpose" class="hash-link" aria-label="Direct link to Where weaker guarantees are chosen on purpose" title="Direct link to Where weaker guarantees are chosen on purpose" translate="no">​</a></h2>
<p>A social media like counter is the canonical case for choosing eventual consistency deliberately, not out of laziness: if a "like" briefly shows 4,201 instead of 4,202 on one replica for a few hundred milliseconds, nobody is harmed and almost nobody notices. What that system gets in exchange is much lower write latency and much higher availability, since it never has to stall a write waiting on every replica to agree.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-strong-consistency-earns-its-cost">Where strong consistency earns its cost<a href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency#where-strong-consistency-earns-its-cost" class="hash-link" aria-label="Direct link to Where strong consistency earns its cost" title="Direct link to Where strong consistency earns its cost" translate="no">​</a></h2>
<p>A bank balance is the opposite case. If a withdrawal is confirmed but a concurrent read against a stale replica shows funds that were already spent, that's not a rounding error — it's a double-spend risk. Inventory counts for a limited item during a flash sale have the same shape: sell the same last unit twice because two replicas disagreed briefly, and you've made a promise you can't keep. These are exactly the cases where paying strong consistency's latency cost is the correct trade, not an overcautious one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="its-a-spectrum-not-a-binary-switch">It's a spectrum, not a binary switch<a href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency#its-a-spectrum-not-a-binary-switch" class="hash-link" aria-label="Direct link to It's a spectrum, not a binary switch" title="Direct link to It's a spectrum, not a binary switch" translate="no">​</a></h2>
<p>Real systems usually don't pick one extreme globally. <strong>Read-your-own-writes</strong> consistency guarantees a client always sees its own recent write, even if other clients might briefly see something older — a common middle ground for user-facing apps. <strong>Quorum-based</strong> systems tune this directly with numbers: with <code>N</code> replicas, requiring <code>W</code> nodes to acknowledge a write and <code>R</code> nodes to agree on a read, setting <code>W + R &gt; N</code> guarantees every read overlaps with the most recent write, letting you dial consistency and latency against each other rather than choosing one extreme for the whole system.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>Naming "we chose eventual consistency" is incomplete without naming <em>why the specific data can tolerate it</em> — and just as importantly, naming which parts of the same system can't. A well-designed system frequently uses both: eventual consistency for a feed or a counter, strong consistency for the parts that touch money or inventory, exactly as the <a class="" href="https://system-design-lab.pages.dev/blog/cap-theorem-explained">CAP theorem</a> framing would predict once you look at each piece of data individually instead of the system as a whole.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/strong-vs-eventual-consistency#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson expands on quorum tuning and read-your-own-writes with more worked examples:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/system-design-tradeoffs/strong-vs-eventual-consistency">Read the full Strong vs Eventual Consistency lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Tradeoffs</category>
        </item>
        <item>
            <title><![CDATA[Synchronous vs Asynchronous Communication Explained]]></title>
            <link>https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication</link>
            <guid>https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Does the caller wait for a response, or move on and find out later? The one underlying decision behind nearly every communication tradeoff in system design.]]></description>
            <content:encoded><![CDATA[<p>Long polling vs. WebSockets, push vs. pull, message queues vs. direct calls — nearly every specific communication tradeoff in system design is really this one underlying decision, applied to a particular piece of a system.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="synchronous-call-wait-continue">Synchronous: call, wait, continue<a href="https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication#synchronous-call-wait-continue" class="hash-link" aria-label="Direct link to Synchronous: call, wait, continue" title="Direct link to Synchronous: call, wait, continue" translate="no">​</a></h2>
<p>A synchronous call blocks the caller until a response comes back — an ordinary API request is the default example: send, then do nothing else until the response (or a timeout) arrives. It's simple to reason about, since code reads top to bottom and each line's result is known before the next runs. But it directly couples the caller's responsiveness to the callee's: if the callee is slow, the caller is slow; if the callee is down, the caller is stuck waiting — exactly the failure mode a <a class="" href="https://system-design-lab.pages.dev/blog/circuit-breaker-pattern">circuit breaker</a> exists to cut short.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="asynchronous-send-move-on-find-out-later">Asynchronous: send, move on, find out later<a href="https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication#asynchronous-send-move-on-find-out-later" class="hash-link" aria-label="Direct link to Asynchronous: send, move on, find out later" title="Direct link to Asynchronous: send, move on, find out later" translate="no">​</a></h2>
<p>An asynchronous call doesn't wait — the caller triggers something (publishing to a message queue, emitting an event) and continues immediately, finding out the result later if it needs to at all. This decouples the caller's responsiveness from the callee's entirely: a slow or temporarily unavailable consumer doesn't block the producer, since the queue or event bus absorbs the gap.</p>
<div class="frame_Uhhe"><div class="header_nh7O"><img src="https://system-design-lab.pages.dev/img/logo.svg" alt="" class="logo_tTla"><span class="wordmark_EJV3">System Design Lab</span></div><div class="body_tyrq"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-real-question-does-the-caller-need-the-answer-right-now">The real question: does the caller need the answer right now?<a href="https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication#the-real-question-does-the-caller-need-the-answer-right-now" class="hash-link" aria-label="Direct link to The real question: does the caller need the answer right now?" title="Direct link to The real question: does the caller need the answer right now?" translate="no">​</a></h2>
<p>Stripped to its core, this is the entire decision. If the caller genuinely needs the result before it can do anything else — checking whether a login succeeded before showing an account page — synchronous is the natural, honest fit, and forcing it asynchronous just adds complexity (polling, a callback) to simulate waiting, for no real benefit. If the caller doesn't need the result immediately — logging an analytics event, sending a confirmation email after signup — synchronous is the wrong default, needlessly coupling the caller's response time to a task it doesn't actually need finished yet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="every-specific-pairing-is-a-version-of-this-one-choice">Every specific pairing is a version of this one choice<a href="https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication#every-specific-pairing-is-a-version-of-this-one-choice" class="hash-link" aria-label="Direct link to Every specific pairing is a version of this one choice" title="Direct link to Every specific pairing is a version of this one choice" translate="no">​</a></h2>
<p>Long polling vs. WebSockets is about how a client finds out about server-side changes — poll synchronously and repeatedly, or receive asynchronously as they happen. Push vs. pull architecture is the same question about who initiates. Event-driven architecture is choosing asynchronous, event-based communication as a whole system's default integration style rather than direct synchronous calls between every service. Recognizing that these are all the same underlying question is what turns a handful of memorized pairs into one coherent way of reasoning about a new tradeoff the moment it comes up.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-asynchronous-communication-actually-costs">What asynchronous communication actually costs<a href="https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication#what-asynchronous-communication-actually-costs" class="hash-link" aria-label="Direct link to What asynchronous communication actually costs" title="Direct link to What asynchronous communication actually costs" translate="no">​</a></h2>
<p>Trading away "wait and know the result immediately" isn't free. A caller that doesn't wait for a result needs a different way to find out about failures — a dead-letter queue, a retry policy — and any operation whose result matters eventually needs idempotency to safely handle the at-least-once delivery that pattern typically implies. Asynchronous communication buys decoupling and resilience to a slow downstream dependency, at the cost of that extra infrastructure and the complexity of answering "how do I find out what actually happened" — a question a synchronous call answers automatically, just by returning.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>Every service-to-service interaction in a design is worth explicitly labeling synchronous or asynchronous, and justifying it by whether the caller genuinely needs the result before proceeding. It's the thread connecting nearly every communication-pattern tradeoff in this course into one coherent way of reasoning, rather than a pile of separately memorized pairs.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/synchronous-vs-asynchronous-communication#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson ties this back to every other communication tradeoff in the course, closing out the System Design Tradeoffs module:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/system-design-tradeoffs/synchronous-vs-asynchronous-communication">Read the full Synchronous vs Asynchronous Communication lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Tradeoffs</category>
        </item>
        <item>
            <title><![CDATA[TCP vs UDP: What's the Actual Difference?]]></title>
            <link>https://system-design-lab.pages.dev/blog/tcp-vs-udp</link>
            <guid>https://system-design-lab.pages.dev/blog/tcp-vs-udp</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[TCP guarantees ordered, reliable delivery via a handshake and retransmission; UDP sends best-effort with near-zero setup cost. Here's when each wins.]]></description>
            <content:encoded><![CDATA[<p>TCP and UDP are both ways to send bytes across a network, but "TCP is reliable, UDP isn't" undersells the actual tradeoff — UDP isn't a worse TCP, it's a deliberate bet that a late packet is worse than a lost one.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-quick-answer">The quick answer<a href="https://system-design-lab.pages.dev/blog/tcp-vs-udp#the-quick-answer" class="hash-link" aria-label="Direct link to The quick answer" title="Direct link to The quick answer" translate="no">​</a></h2>
<table><thead><tr><th></th><th>TCP</th><th>UDP</th></tr></thead><tbody><tr><td>Connection</td><td>Established via a 3-way handshake first</td><td>Connectionless — just send</td></tr><tr><td>Ordering</td><td>Guaranteed</td><td>Not guaranteed</td></tr><tr><td>Reliability</td><td>Lost packets are retransmitted</td><td>Best-effort, no retransmission</td></tr><tr><td>Congestion control</td><td>Built in, backs off under loss</td><td>None</td></tr><tr><td>Header overhead</td><td>Larger</td><td>Minimal</td></tr><tr><td>Typical use</td><td>Web pages, APIs, file transfer</td><td>Video calls, DNS, HTTP/3 (QUIC)</td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-tcp-actually-does-to-earn-reliable">What TCP actually does to earn "reliable"<a href="https://system-design-lab.pages.dev/blog/tcp-vs-udp#what-tcp-actually-does-to-earn-reliable" class="hash-link" aria-label="Direct link to What TCP actually does to earn &quot;reliable&quot;" title="Direct link to What TCP actually does to earn &quot;reliable&quot;" translate="no">​</a></h2>
<p>TCP opens every connection with a three-way handshake (SYN, SYN-ACK, ACK) before a single byte of real data moves, which is real latency spent up front in exchange for a guarantee. Every segment gets a sequence number, the receiver acknowledges what it got, and anything unacknowledged within a timeout gets retransmitted — so data arrives complete and in order, or the connection reports a failure. TCP also runs congestion control, backing off its send rate when it detects loss, so it behaves cooperatively on a shared network instead of hammering it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-udp-does-instead-almost-nothing-on-purpose">What UDP does instead: almost nothing, on purpose<a href="https://system-design-lab.pages.dev/blog/tcp-vs-udp#what-udp-does-instead-almost-nothing-on-purpose" class="hash-link" aria-label="Direct link to What UDP does instead: almost nothing, on purpose" title="Direct link to What UDP does instead: almost nothing, on purpose" translate="no">​</a></h2>
<p>UDP has no handshake, no sequence numbers, no acknowledgments, no retransmission, and no congestion control. A sender just fires packets; if one is dropped, nobody notices at the transport layer — it's simply gone. This sounds like a strictly worse protocol until you consider what it buys: near-zero setup latency (no handshake to wait on) and a much smaller header, which matters when you're sending many small, time-sensitive packets.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-real-question-is-a-late-packet-worse-than-a-lost-one">The real question: is a late packet worse than a lost one?<a href="https://system-design-lab.pages.dev/blog/tcp-vs-udp#the-real-question-is-a-late-packet-worse-than-a-lost-one" class="hash-link" aria-label="Direct link to The real question: is a late packet worse than a lost one?" title="Direct link to The real question: is a late packet worse than a lost one?" translate="no">​</a></h2>
<p>This is the actual decision, and it's the reason UDP exists at all. For a video call, a frame that arrives 400ms late is useless — the moment it was meant to represent has already passed, and TCP-style "wait and retransmit" logic would just add growing delay for stale data nobody wants. Dropping that frame and moving on to the next one, which is exactly what UDP-based protocols do, produces a <em>better</em> user experience than perfect, ordered, retransmitted delivery. The same logic applies to live audio, to gaming, and to DNS — a UDP-based DNS query that gets no response can just be reissued immediately, which is often faster than waiting for TCP's own retransmission timers.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-http3-picked-udp">Why HTTP/3 picked UDP<a href="https://system-design-lab.pages.dev/blog/tcp-vs-udp#why-http3-picked-udp" class="hash-link" aria-label="Direct link to Why HTTP/3 picked UDP" title="Direct link to Why HTTP/3 picked UDP" translate="no">​</a></h2>
<p>Modern HTTP/3 is built on QUIC, which runs over UDP rather than TCP — deliberately. TCP's ordering guarantee has a side effect called head-of-line blocking: if one packet is lost, everything behind it in the stream has to wait for the retransmit before the application sees any of it, even data for a completely unrelated request multiplexed on the same connection. QUIC implements its own reliability and ordering <em>on top of</em> UDP, but per-stream instead of per-connection, so one lost packet only blocks the stream it belongs to. It's a case of taking UDP's minimal transport and rebuilding just the pieces of TCP's guarantees that are actually needed, without the parts that hurt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/tcp-vs-udp#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>Naming "TCP" or "UDP" alone is a weak answer. The stronger one names <em>why</em> the specific workload tolerates loss or doesn't: a chat message needs guaranteed, ordered delivery (TCP), a live video stream tolerates a dropped frame far better than a delayed one (UDP), and a file upload absolutely cannot silently drop bytes (TCP). That reasoning — not the protocol name — is what the interview is actually testing.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/tcp-vs-udp#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson includes a sequence diagram comparing the TCP handshake against UDP's connectionless send, and goes deeper on where each protocol shows up in real systems:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/networking-fundamentals/tcp-vs-udp">Read the full TCP vs UDP lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Networking</category>
        </item>
        <item>
            <title><![CDATA[What Is a CDN? A Simple Explanation]]></title>
            <link>https://system-design-lab.pages.dev/blog/what-is-a-cdn</link>
            <guid>https://system-design-lab.pages.dev/blog/what-is-a-cdn</guid>
            <pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A CDN caches content at edge servers close to users, sidestepping the speed-of-light latency floor no amount of server optimization can fix.]]></description>
            <content:encoded><![CDATA[<p>Most caching is about skipping work — a database query, a computation. A CDN caches something different: distance itself, the physical time it takes a signal to travel across the planet.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-hard-floor-no-server-can-optimize-away">The hard floor no server can optimize away<a href="https://system-design-lab.pages.dev/blog/what-is-a-cdn#the-hard-floor-no-server-can-optimize-away" class="hash-link" aria-label="Direct link to The hard floor no server can optimize away" title="Direct link to The hard floor no server can optimize away" translate="no">​</a></h2>
<p>Even at the speed of light, a round trip between New York and Sydney costs over 100ms from distance alone, before either server does a millisecond of actual work. No amount of backend optimization changes that — it's a physical constant, not an engineering problem. A CDN's entire value proposition is sidestepping it: instead of every request traveling all the way to one origin server, it travels to whichever <strong>edge server</strong> (or point of presence) is geographically nearest, and only that edge server occasionally talks back to the origin.</p>
<div class="frame_Uhhe"><div class="header_nh7O"><img src="https://system-design-lab.pages.dev/img/logo.svg" alt="" class="logo_tTla"><span class="wordmark_EJV3">System Design Lab</span></div><div class="body_tyrq"></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-a-user-actually-reaches-the-nearest-edge">How a user actually reaches the nearest edge<a href="https://system-design-lab.pages.dev/blog/what-is-a-cdn#how-a-user-actually-reaches-the-nearest-edge" class="hash-link" aria-label="Direct link to How a user actually reaches the nearest edge" title="Direct link to How a user actually reaches the nearest edge" translate="no">​</a></h2>
<p>Getting each user routed to their closest edge location is itself a solved problem, built on infrastructure this course already covers: <strong>geo-DNS</strong> resolves the same domain name to a different IP address depending on where the query originated, pointing a user at the nearest edge before a single byte of the real request is sent. It's the same DNS-based load-balancing idea from ordinary <a class="" href="https://system-design-lab.pages.dev/blog/load-balancing-l4-vs-l7">load balancing</a>, just applied at planetary scale instead of within one datacenter.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="static-content-is-the-easy-case--dynamic-content-isnt">Static content is the easy case — dynamic content isn't<a href="https://system-design-lab.pages.dev/blog/what-is-a-cdn#static-content-is-the-easy-case--dynamic-content-isnt" class="hash-link" aria-label="Direct link to Static content is the easy case — dynamic content isn't" title="Direct link to Static content is the easy case — dynamic content isn't" translate="no">​</a></h2>
<p>CDNs were built for, and remain best at, <strong>static content</strong>: images, video, CSS, JS bundles — anything identical for every user that changes infrequently. That content can sit at the edge behind a simple TTL and get served without ever touching the origin. <strong>Dynamic, personalized content</strong> — a user's own account page, a live search result — is a much harder fit: caching it at the edge risks serving one user's private data to someone else, so CDNs either skip caching it entirely (proxying straight through) or use narrower techniques like edge computing to personalize responses without a full round trip home.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="a-side-effect-worth-naming-origin-protection">A side effect worth naming: origin protection<a href="https://system-design-lab.pages.dev/blog/what-is-a-cdn#a-side-effect-worth-naming-origin-protection" class="hash-link" aria-label="Direct link to A side effect worth naming: origin protection" title="Direct link to A side effect worth naming: origin protection" translate="no">​</a></h2>
<p>Because most cacheable requests get absorbed at the edge, the origin server only ever sees cache misses and genuinely dynamic traffic — often a small fraction of the total. It's the same load-shedding effect any cache has on the system behind it, just operating at the scale of an entire global user base. It also happens to be a meaningful defense against traffic spikes and some categories of denial-of-service traffic, since the public-facing surface is the CDN's much larger edge fleet, not the origin itself.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="push-vs-pull">Push vs. pull<a href="https://system-design-lab.pages.dev/blog/what-is-a-cdn#push-vs-pull" class="hash-link" aria-label="Direct link to Push vs. pull" title="Direct link to Push vs. pull" translate="no">​</a></h2>
<p>CDNs populate edge caches one of two ways: <strong>pull</strong>, where an edge server lazily fetches and caches content the first time it's requested (the standard cache-aside pattern, applied at the edge), or <strong>push</strong>, where content is proactively uploaded to every edge location ahead of time. Pull is the simpler default; push is used when content has to be guaranteed present before the first request — a scheduled video release, for instance.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-matters-in-an-interview">Why this matters in an interview<a href="https://system-design-lab.pages.dev/blog/what-is-a-cdn#why-this-matters-in-an-interview" class="hash-link" aria-label="Direct link to Why this matters in an interview" title="Direct link to Why this matters in an interview" translate="no">​</a></h2>
<p>Any design serving a geographically distributed audience — especially one with real static assets — should name a CDN explicitly rather than leaving "the client fetches this over the internet" implicit. Just as important: being ready to draw the static/dynamic line, naming which parts of a design a CDN genuinely helps and which parts still have to hit the origin, is what separates a real answer from name-dropping "CDN" as a buzzword.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="go-deeper">Go deeper<a href="https://system-design-lab.pages.dev/blog/what-is-a-cdn#go-deeper" class="hash-link" aria-label="Direct link to Go deeper" title="Direct link to Go deeper" translate="no">​</a></h2>
<p>The full lesson covers push vs. pull CDNs in more depth and connects back to the general push/pull architecture tradeoff:</p>
<p>👉 <strong><a class="" href="https://system-design-lab.pages.dev/docs/caching-fundamentals/cdn">Read the full CDN lesson</a></strong> — part of the free <a class="" href="https://system-design-lab.pages.dev/">System Design Lab</a> course.</p>]]></content:encoded>
            <category>Caching</category>
        </item>
    </channel>
</rss>