<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Emily Woods</title>
    <description>The latest articles on DEV Community by Emily Woods (@emilywoodsnyc).</description>
    <link>https://dev.to/emilywoodsnyc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3912599%2Fc95a7ee1-41d2-4e1e-994d-e1aedab86d39.png</url>
      <title>DEV Community: Emily Woods</title>
      <link>https://dev.to/emilywoodsnyc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emilywoodsnyc"/>
    <language>en</language>
    <item>
      <title>Backend Interview Question: Your API Takes 900 ms. SQL Takes 40.</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Thu, 12 Nov 2026 21:55:04 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/backend-interview-question-your-api-takes-900-ms-sql-takes-40-2cfi</link>
      <guid>https://dev.to/emilywoodsnyc/backend-interview-question-your-api-takes-900-ms-sql-takes-40-2cfi</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;Before adding a cache, build a request timeline and find the work your user is actually waiting for.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Someone says the API is slow, and suddenly the room has six solutions. Add Redis, add an index, add more servers, rewrite the endpoint, parallelize everything, or blame the network and hope nobody asks a follow-up.&lt;/p&gt;

&lt;p&gt;Meanwhile, we still have not explained where the time went.&lt;/p&gt;

&lt;p&gt;I like a much smaller starting point. Give me one slow request, the boundary we are timing, and enough evidence to account for its elapsed time. Then we can have a useful conversation about what to change.&lt;/p&gt;

&lt;p&gt;For this original backend interview exercise, imagine a product-details API that takes 900 milliseconds from gateway arrival to completion of the response. Its SQL query uses 40 milliseconds. Those numbers are a synthetic fixture, not a production incident or a claim about a particular company.&lt;/p&gt;

&lt;p&gt;Our job is to improve the experience without accidentally making the answer less correct. A faster response containing the wrong price is not the little victory we are looking for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8yr94wogb972aocex3ok.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8yr94wogb972aocex3ok.png" alt="A large 900 ms API stopwatch contrasts with a 40 ms database query, asking where the remaining time went." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the stopwatch before choosing the fix
&lt;/h2&gt;

&lt;p&gt;First, clarify where the measurement starts and stops. Browser click to usable screen, gateway arrival to completed response, and database query execution are different intervals.&lt;/p&gt;

&lt;p&gt;For our example, we are timing the server path from the gateway receiving the request until the response finishes there. We are not yet measuring DNS, connection setup before arrival, the network trip back to the browser, or rendering in the client.&lt;/p&gt;

&lt;p&gt;That boundary matters because a beautiful server trace cannot explain time it never measured.&lt;/p&gt;

&lt;p&gt;Next, make the complaint specific. Is this one route, one region, one tenant, or every request? Did the slowdown begin after a deployment, appear only during busy periods, or affect unusually large responses? Compare successful and failed requests separately so fast failures do not make the service look reassuringly quick.&lt;/p&gt;

&lt;p&gt;Google’s SRE guidance on &lt;a href="https://sre.google/sre-book/monitoring-distributed-systems/" rel="noopener noreferrer"&gt;monitoring distributed systems&lt;/a&gt; emphasizes latency alongside traffic, errors, and saturation. Those signals help us describe the symptom before choosing a remedy.&lt;/p&gt;

&lt;p&gt;For this exercise, start with one successful slow request. Treat it as a clue to investigate, not proof that every user has the same problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every part of the 900 ms a place on the page
&lt;/h2&gt;

&lt;p&gt;Imagine our instrumentation gives us the following non-overlapping stages. In this deliberately simple fixture, they happen sequentially.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request waits 120 ms before an application worker starts.&lt;/li&gt;
&lt;li&gt;Authentication takes 40 ms.&lt;/li&gt;
&lt;li&gt;The SQL query takes 40 ms.&lt;/li&gt;
&lt;li&gt;An outbound dependency call takes 600 ms as observed by our service.&lt;/li&gt;
&lt;li&gt;Serialization and response completion take the remaining 100 ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, those stages account for all 900 ms. We are assuming that the measurement includes the queue and that no relevant interval is missing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzrz71ukqntr10dqt7zng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzrz71ukqntr10dqt7zng.png" alt="Five sequential stages account for the synthetic 900 ms request, with the dependency call consuming 600 ms." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your real trace does not add up, resist the urge to name the missing time network overhead and move on. It could be uninstrumented work, a gap between spans, or a boundary you misunderstood.&lt;/p&gt;

&lt;p&gt;OpenTelemetry’s &lt;a href="https://opentelemetry.io/docs/concepts/signals/traces/" rel="noopener noreferrer"&gt;explanation of traces and spans&lt;/a&gt; is useful here. A trace connects operations through a request, while spans describe individual operations with timing and context. That structure helps you ask where the request went, but it cannot reveal work you never instrumented.&lt;/p&gt;

&lt;p&gt;Also check what the 600 ms actually measures. An HTTP client span can include time spent acquiring a connection, connecting, waiting for the remote service, and receiving its response. It is evidence that this interval is expensive, not proof that the remote application spent 600 ms computing.&lt;/p&gt;

&lt;p&gt;Honestly, this is where the investigation starts becoming interesting. We have narrowed the search without pretending we already know the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put an upper bound on your favorite optimization
&lt;/h2&gt;

&lt;p&gt;Suppose the first suggestion is to cache the SQL result. Even if that eliminated the entire 40 ms query at zero added cost, our fixed trace would still take 860 ms.&lt;/p&gt;

&lt;p&gt;That is a maximum saving of about 4.4% for this example, with every other stage held constant. It is not a prediction of a real cache’s performance, and it says nothing yet about cache misses, invalidation, or freshness.&lt;/p&gt;

&lt;p&gt;By comparison, reducing the dependency interval from 600 to 200 ms would bring this same simplified timeline to 500 ms. That is a hypothesis worth investigating because the potential impact is much larger.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fraasxuiztrzgzusaslm5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fraasxuiztrzgzusaslm5.png" alt="Two calculations contrast the 40 ms maximum SQL saving with a hypothetical 400 ms dependency improvement, holding other stages constant." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This does not mean caching is always the wrong answer. Under load, removing database work might reduce contention and shorten other waits too. Our one-request calculation deliberately holds those effects constant, so a load experiment could reveal a different outcome.&lt;/p&gt;

&lt;p&gt;What the calculation does give us is a disciplined question. What mechanism makes this change reduce the delay we actually observed, and how much improvement is plausible if that mechanism works?&lt;/p&gt;

&lt;p&gt;I prefer answering that before introducing another service we will need to operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the waiting time instead of blaming the biggest box
&lt;/h2&gt;

&lt;p&gt;Take the 600 ms dependency interval and split the next investigation into possibilities. Maybe the local client waits for a connection. Maybe connection setup is slow. Maybe the remote service is slow. Maybe a large response takes time to arrive.&lt;/p&gt;

&lt;p&gt;Each explanation needs different evidence and suggests a different change.&lt;/p&gt;

&lt;p&gt;For example, if connection acquisition dominates, inspect pool occupancy, pending requests, and connection lifetime. Increasing the pool size is only a proposal; it might overload the downstream service or move the queue somewhere else.&lt;/p&gt;

&lt;p&gt;If remote processing dominates, correlate the outbound request with the downstream trace when available. If payload transfer dominates, inspect response size and whether the caller needs every field. Keep missing evidence visible rather than promoting a plausible explanation into a fact.&lt;/p&gt;

&lt;p&gt;Do the same for the 120 ms before the application worker starts. Low CPU does not rule out waiting on a constrained resource. On the other hand, a single queued request does not prove the service needs more workers.&lt;/p&gt;

&lt;p&gt;Your next measurement should distinguish between competing explanations. Adding ten dashboard screenshots that all say slow is not the same as narrowing the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel work changes the arithmetic
&lt;/h2&gt;

&lt;p&gt;Now change the example. Suppose two independent downstream calls start together, one taking 400 ms and the other 300 ms. The response needs both results, followed by 100 ms of response work.&lt;/p&gt;

&lt;p&gt;Ignoring other overhead, elapsed time is 500 ms because the response waits for the slower branch and then finishes its own work. Adding 400, 300, and 100 to report 800 ms would double-count overlap.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhccdzqamaeq7abxvf5av.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhccdzqamaeq7abxvf5av.png" alt="Parallel 400 ms and 300 ms calls join before 100 ms of response work, producing 500 ms of elapsed time in the illustrative model." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That also changes which optimization matters. Making the 300 ms branch take 100 ms would not shorten this particular response while the 400 ms branch still determines the join. It could reduce resource use, but that is a different claim.&lt;/p&gt;

&lt;p&gt;Before parallelizing real calls, establish that neither depends on the other’s result and that concurrency will not violate ordering or overload a shared dependency. Parallel is a scheduling decision, not a synonym for faster in every situation.&lt;/p&gt;

&lt;p&gt;Read a request timeline as a set of dependencies. Find the chain that determines when the response can finish, and avoid adding parent spans to the child spans already contained within them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the experiment before celebrating the improvement
&lt;/h2&gt;

&lt;p&gt;For our synthetic request, a useful investigation note might say that the dependency interval is 600 ms, connection-pool waiting is one possible explanation, and acquisition timing is the next evidence needed. That is a plan somebody else can question and reproduce.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo97r3a6rs5o5ph8uoz9w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo97r3a6rs5o5ph8uoz9w.png" alt="An investigation worksheet separates the observed 600 ms span, a connection-pool hypothesis, the next measurement, and the success check." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the evidence supports a change, compare equivalent requests under equivalent conditions. Keep the route, payload, authorization context, data shape, and load profile clear. Separate warm and cold behavior when they differ.&lt;/p&gt;

&lt;p&gt;Check end-to-end latency along with errors, correctness, completed throughput, and the resource you changed. Include slow requests in the comparison rather than relying only on the average, and record the sample size and measurement window. An average improvement can hide a worse experience for a smaller group.&lt;/p&gt;

&lt;p&gt;A patch that returns early by skipping required work is a behavior change, not a free optimization. If the product accepts a partial response or stale data, state that contract explicitly and test it.&lt;/p&gt;

&lt;p&gt;For another practice round, use this &lt;a href="https://prachub.com/interview-questions/improve-shopping-cart-latency-and-stock-notifications?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;shopping cart latency problem&lt;/a&gt;. Draw the current request path before proposing improvements, then identify which work must finish before the user can see a correct cart. Treat stock freshness as part of the result you must preserve.&lt;/p&gt;

&lt;p&gt;Before moving on, leave yourself a short investigation note with the measurement boundary, the observed timeline, one hypothesis, the next piece of evidence, and the result that would count as success.&lt;/p&gt;

&lt;p&gt;That is the interview answer I want to practice. We can absolutely talk about caches and indexes, but first let’s make every millisecond in the complaint earn its place on the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
    </item>
    <item>
      <title>System Design Interview: Your 100-Request Limit Just Allowed 200</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Thu, 12 Nov 2026 05:17:08 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/system-design-interview-your-100-request-limit-just-allowed-200-4hai</link>
      <guid>https://dev.to/emilywoodsnyc/system-design-interview-your-100-request-limit-just-allowed-200-4hai</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;One boundary example, a small Python model, and a better way to explain what your rate limiter actually promises.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;You set the limit to 100 requests per minute, check that request 101 gets rejected, and feel pretty good about the implementation.&lt;/p&gt;

&lt;p&gt;Then someone sends 100 requests just before the minute ends and another 100 just after the next minute begins. Your service accepts all 200, even though only a fraction of a second has passed.&lt;/p&gt;

&lt;p&gt;Nobody hacked the counter. It followed the rule you implemented, which may not be the rule you meant.&lt;/p&gt;

&lt;p&gt;I like this system design exercise because the interesting part arrives before Redis, sharding, or a beautifully drawn API gateway. We have to decide what per minute means, and that tiny phrase changes almost everything that follows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdi9has1sp8la17ecc81.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdi9has1sp8la17ecc81.png" alt="A clock boundary separates a configured limit of 100 from 200 accepted requests, introducing the rate-limiter interview exercise." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the timing awkward before making the architecture impressive
&lt;/h2&gt;

&lt;p&gt;Use an original practice scenario with one authenticated client and one endpoint. Every admitted request costs one unit, and rejected attempts do not consume this particular quota. Other abuse controls may count attempts differently.&lt;/p&gt;

&lt;p&gt;Suppose the implementation uses fixed windows from 0 to 60 seconds, then 60 to 120 seconds. Each window has its own counter, capped at 100.&lt;/p&gt;

&lt;p&gt;At 59.9 seconds, the client sends 100 requests. At 60.1 seconds, it sends another 100. Both windows remain within their individual limits, but the service has admitted 200 requests across 0.2 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohclcxq6a78wndada900.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohclcxq6a78wndada900.png" alt="Two fixed windows each admit 100 requests near their shared boundary, totaling 200 across 0.2 seconds." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That behavior is not automatically a defective fixed-window implementation. It is a mismatch if the product promise was at most 100 admissions during any rolling 60-second interval.&lt;/p&gt;

&lt;p&gt;For our exercise, choose that rolling promise explicitly. At decision time t, count admissions in the interval (t − 60 seconds, t]. An admission exactly 60 seconds old has expired. A request arriving now belongs to the interval if we admit it.&lt;/p&gt;

&lt;p&gt;Honestly, writing down that boundary is more useful than listing five algorithm names and hoping one sounds senior enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a little history instead of resetting the calendar
&lt;/h2&gt;

&lt;p&gt;A sliding log stores the timestamps of admitted requests. Before deciding on a new request, remove expired entries, count what remains, and admit only if there is room. Record the new timestamp only after that decision.&lt;/p&gt;

&lt;p&gt;In our 59.9-second example, the first 100 timestamps are still recent at 60.1 seconds. The second burst is rejected. Those entries expire at 119.9 seconds under the boundary rule we chose.&lt;/p&gt;

&lt;p&gt;Redis’s &lt;a href="https://redis.io/tutorials/howtos/ratelimiting/" rel="noopener noreferrer"&gt;rate-limiting walkthrough&lt;/a&gt; explains the different algorithms and their trade-offs. I prefer using a concrete timeline like this before selecting one, because an implementation can be correct for one policy and unsuitable for another.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzu9p86oajixhb7zuhzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzu9p86oajixhb7zuhzo.png" alt="A rolling interval at 60 seconds removes the timestamp at zero while retaining timestamps inside the open-left, closed-right interval." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a small reference model for one quota key. It uses integer milliseconds and caller-supplied, nondecreasing decision times so the boundary is easy to test without waiting for a real clock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RollingLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;window_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'limits must be positive'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window_ms&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admitted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_time&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'time must not move backward'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now_ms&lt;/span&gt;
        &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now_ms&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_ms&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admitted&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admitted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admitted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admitted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admitted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The less-than-or-equal comparison is doing real work. Changing it to a strict less-than would keep a request exactly one window old and enforce a different boundary.&lt;/p&gt;

&lt;p&gt;This model assumes sequential calls in one process. It is not thread safe, distributed, persistent, or a production middleware. Its job is to make the admission rule executable before we add those responsibilities.&lt;/p&gt;

&lt;p&gt;Each admitted timestamp enters and leaves the deque once, giving amortized constant time per call over a sequence of calls. One call can still remove up to the limit’s worth of expired timestamps. Stored history is bounded by the admission limit for this one key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the edge without sleeping for a minute
&lt;/h2&gt;

&lt;p&gt;You can run these checks immediately after the class. They separate the fixed-window boundary burst from the exact point when old admissions expire.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RollingLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;59_900&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60_100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;119_899&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;119_900&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RollingLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;59_999&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also try multiple requests with the same timestamp. Time equality must not make distinct requests disappear. In this deque they remain separate entries; if you later use a sorted set, a timestamp alone is not a safe unique member identifier for all requests arriving at that time.&lt;/p&gt;

&lt;p&gt;Keep the test clock under your control. Sleeping introduces scheduling noise while teaching very little about the contract you are trying to verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three servers quietly change the meaning of your limit
&lt;/h2&gt;

&lt;p&gt;Now create three independent copies of the model behind a load balancer. If the same client reaches all three, each copy can admit 100 requests. You have implemented three local allowances, which can add up to 300.&lt;/p&gt;

&lt;p&gt;Our original promise applied to the client’s total traffic, so local correctness is no longer enough.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysyp6v3qrgl5stk57l77.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysyp6v3qrgl5stk57l77.png" alt="Three independent server allowances can admit 300 total requests; a shared atomic decision keeps one quota for the client." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One design is to route decisions for a quota key to authoritative shared state. The sequence that removes expired entries, checks the count, and records an admission must execute atomically. Separate network calls can let two requests both observe the final free slot and both take it.&lt;/p&gt;

&lt;p&gt;A short Redis script is one implementation option. Redis documents &lt;a href="https://redis.io/docs/latest/develop/programmability/eval-intro/" rel="noopener noreferrer"&gt;atomic script execution&lt;/a&gt;, but a script is not a complete availability strategy. Keep its work bounded, and decide what state loss or failover means for the quota.&lt;/p&gt;

&lt;p&gt;In a sorted-set version, timestamps can be scores while distinct admission identifiers are members. Partition by the quota key so the state needed for one decision stays together. A particularly busy key can still become a hot spot; adding shards does not split an indivisible decision for free.&lt;/p&gt;

&lt;p&gt;Client identity needs care too. A browser-provided string should not let someone choose a new quota whenever they want. Tie this example’s key to the authenticated account and endpoint policy. In a multi-tenant service, include the tenant boundary rather than accidentally sharing somebody else’s allowance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what happens when the limiter cannot decide
&lt;/h2&gt;

&lt;p&gt;If the shared store times out, there is no magical response that preserves every desirable property.&lt;/p&gt;

&lt;p&gt;Failing open admits traffic while the quota cannot be checked, which sacrifices strict enforcement. Failing closed protects that boundary by refusing traffic, which also affects legitimate clients. A reduced local allowance is another explicit compromise, but it is not equivalent to the original global promise.&lt;/p&gt;

&lt;p&gt;For this practice scenario, assume the endpoint triggers expensive work and strict admission control matters more than serving through a limiter outage. We will refuse new work when the limiter is unavailable, and distinguish that infrastructure failure from a known quota rejection in the response and metrics. Different product priorities can justify another decision.&lt;/p&gt;

&lt;p&gt;Use a consistent time authority for distributed decisions and define behavior for clock corrections. The monotonic sequence we supplied to the Python model does not appear automatically when several machines contribute timestamps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0c87xd1uzghqnf4nj2s9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0c87xd1uzghqnf4nj2s9.png" alt="A test table covers simultaneous admissions, rejection before expiry, exact expiry, shared quotas across servers, and an unavailable limiter store." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A rolling quota still allows a burst, and that may be the next question
&lt;/h2&gt;

&lt;p&gt;Our model admits 100 requests at the same instant when the history is empty. It prevents the 200-request boundary trick, but it does not spread those first 100 requests evenly over a minute.&lt;/p&gt;

&lt;p&gt;That distinction matters if each request launches expensive work. A separate concurrency cap limits how much work is active at once; a rate quota controls admissions over time. Neither one automatically replaces the other.&lt;/p&gt;

&lt;p&gt;If the desired policy is a controlled burst plus a sustained refill rate, discuss a token bucket instead. If precise rolling counts are unnecessary, a simpler counter may be appropriate. Choose the promise first and explain the cost of keeping it, rather than calling one algorithm universally best.&lt;/p&gt;

&lt;p&gt;To rehearse the full conversation, use this &lt;a href="https://prachub.com/interview-questions/design-a-rate-limiter-algorithm-trade-offs-scalability-and-availability?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;rate-limiter design prompt&lt;/a&gt; and change one assumption at a time. Start with one client, then add several servers, then make the shared store unavailable. Keep your original promise visible while explaining which part survives each change.&lt;/p&gt;

&lt;p&gt;Before closing your notebook, write the interval boundary, the quota key, the atomic decision, and the failure policy in ordinary sentences. If any of them remains vague, you have found the next thing to practice.&lt;/p&gt;

&lt;p&gt;I care much more about that explanation than whether the diagram contains the trendiest database. A counter can be perfectly implemented and still enforce the wrong agreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
    </item>
    <item>
      <title>Before Your Next System Design Interview, Open These 3 Free Resources</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Wed, 11 Nov 2026 17:52:31 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/before-your-next-system-design-interview-open-these-3-free-resources-4ndl</link>
      <guid>https://dev.to/emilywoodsnyc/before-your-next-system-design-interview-open-these-3-free-resources-4ndl</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;Google, AWS, and PostgreSQL reading paired with small exercises that leave you with something better than another bookmark.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;You can recognize every box in a system design diagram and still get uncomfortable when someone asks why it belongs there.&lt;/p&gt;

&lt;p&gt;A cache makes sense until somebody asks how stale the result can be. Retries sound sensible until the first request succeeds and its response disappears. An index looks helpful until you have to explain whether the database actually uses it.&lt;/p&gt;

&lt;p&gt;Honestly, this is where I want preparation to become a little less decorative.&lt;/p&gt;

&lt;p&gt;I like a resource more when it helps me produce a decision I can defend. Reading a chapter is a perfectly nice afternoon, but writing down what counts as a good outcome, drawing a retry timeline, or inspecting a query plan gives the reading somewhere to land.&lt;/p&gt;

&lt;p&gt;These three resources are publicly readable without buying a course. They cover different pieces of the conversation, and each comes with a small practice task of my own. You do not need an AWS account or paid cloud infrastructure for the exercises below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fum6slhkf26apw8nor9z0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fum6slhkf26apw8nor9z0.png" alt="Three notebook cards introduce free system-design reading from Google, AWS, and PostgreSQL, focused on reliability, safe retries, and query plans." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with Google’s SRE Workbook when reliable feels too vague
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://sre.google/workbook/implementing-slos/" rel="noopener noreferrer"&gt;Implementing SLOs in Google’s SRE Workbook&lt;/a&gt;, particularly the discussion of what to measure and the distinction between an indicator and an objective. It is useful for turning a broad reliability aspiration into a measurable service promise.&lt;/p&gt;

&lt;p&gt;You do not need to read the whole book before making this practical. Pick one user journey and decide which outcomes belong in the calculation.&lt;/p&gt;

&lt;p&gt;For an original exercise, imagine a small document app. A signed-in user opens a document they are allowed to read. Our illustrative proposal is that 99.5% of eligible load attempts return the correct document within 500 milliseconds over a rolling 28-day window.&lt;/p&gt;

&lt;p&gt;The percentage and timing are practice assumptions, not targets Google recommends for every product. Write down why the user would care about them and what evidence you would need before adopting them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm620c4oy02oj3vsplm8f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm620c4oy02oj3vsplm8f.png" alt="A service-promise card defines the document-loading journey, a good outcome, eligible attempts, and an illustrative target." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now make the denominator less slippery. An eligible attempt in this exercise is a valid request from an authorized user for an existing document. Server errors and timeouts on those attempts remain in the denominator. Removing them would make the service look healthier by hiding the very failures we care about.&lt;/p&gt;

&lt;p&gt;For the exercise, measure elapsed time from the user action until the document is usable in the client. Record a timeout as a bad outcome at the agreed timeout threshold. This is different from timing only the database query or only a successful server response.&lt;/p&gt;

&lt;p&gt;Try a tiny counting check. If 20,000 eligible attempts occur, a 99.5% target leaves room for 100 bad outcomes. That is a request-count calculation; it does not translate directly into a fixed number of outage minutes.&lt;/p&gt;

&lt;p&gt;Your output is a short service promise card containing the journey, eligibility rule, good-outcome rule, measurement point, and window. Then explain one case your proposed telemetry could miss. That last part keeps a tidy definition from becoming false confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the AWS Builders’ Library when retry sounds like the whole answer
&lt;/h2&gt;

&lt;p&gt;Next, read &lt;a href="https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/" rel="noopener noreferrer"&gt;Making retries safe with idempotent APIs&lt;/a&gt;. Focus on caller-provided request identifiers, late requests, and the distinction between reusing an identifier and expressing a new intent.&lt;/p&gt;

&lt;p&gt;This is a good companion to the first exercise because a reliable-looking response is not enough if repeating the operation creates an extra resource.&lt;/p&gt;

&lt;p&gt;Keep the same document app. Imagine the user creates a document, the server commits it, and the response is lost. The client cannot tell whether creation happened, so it retries.&lt;/p&gt;

&lt;p&gt;For our practice contract, the client sends a key called K7. The server records that K7 belongs to document D42 for this account and this request intent. A repeated request with the same key and intent resolves to that same operation. A genuinely new document needs a new key, even if its title happens to match.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3lj5zjr2xltjnmckqfsw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3lj5zjr2xltjnmckqfsw.png" alt="Three requests using key K7 resolve to document D42, while a changed intent with the same key is rejected under the proposed contract." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Draw three attempts on paper, including the lost response. Under each attempt, write the document count and the identity returned to the caller. We want one document, not three successful-looking acknowledgements for three accidental creations.&lt;/p&gt;

&lt;p&gt;Then make the exercise less polite. Send the same key with different contents and decide the conflict response. Let two identical requests arrive concurrently and explain how the server prevents both from independently creating a document. Finally, let a retry arrive after the stored key has expired and state what the contract can still guarantee.&lt;/p&gt;

&lt;p&gt;Keep these as explicit design questions rather than quietly assuming a dictionary solves them. In a real implementation, coordinating the key record with the resource creation and handling in-progress operations require deliberate storage semantics.&lt;/p&gt;

&lt;p&gt;Your output is a retry timeline with expected outcomes and unresolved decisions. You can do this entire exercise with a notebook. The reading contains cloud examples, but reproducing their infrastructure is not necessary for this practice task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open PostgreSQL’s EXPLAIN guide when add an index becomes a reflex
&lt;/h2&gt;

&lt;p&gt;Use the PostgreSQL documentation on &lt;a href="https://www.postgresql.org/docs/18/using-explain.html" rel="noopener noreferrer"&gt;Using EXPLAIN&lt;/a&gt;. Start with the basics and the EXPLAIN ANALYZE section. The important distinction is between a planner’s estimates and measurements gathered by actually executing a query.&lt;/p&gt;

&lt;p&gt;This exercise assumes you already have a local PostgreSQL database and understand basic SELECT queries. Reading the guide is free; the hands-on part needs a local installation or another environment you control.&lt;/p&gt;

&lt;p&gt;Return to the document app. Its recent-documents view filters by owner, orders by update time, and shows the first twenty results. Sketch the query and predict where the work will happen before inspecting a plan.&lt;/p&gt;

&lt;p&gt;For your own fixture, choose an owner distribution and enough rows to make the experiment meaningful. Keep the fixture disposable. A table with ten rows may quite reasonably make an index look unnecessary, so record the dataset size rather than treating any one plan as universal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5gofe199zxwmytya1oi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5gofe199zxwmytya1oi.png" alt="A local query experiment compares scan type, row counts, buffers, and time before and after one deliberate change, with no invented benchmark results." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a manageable experiment you can repeat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save the query, dataset description, PostgreSQL version, and initial plan.&lt;/li&gt;
&lt;li&gt;Propose one index that matches the filter and ordering, and explain the work you expect it to reduce.&lt;/li&gt;
&lt;li&gt;Refresh relevant statistics after loading the fixture, then compare plans and results under the same conditions.&lt;/li&gt;
&lt;li&gt;Check both correctness and the measured work, including whether the improvement remains useful for a different owner with many more documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use EXPLAIN ANALYZE only on a safe local query you intend to execute, because it runs the statement. Planner costs are not milliseconds, and one faster timing is not enough to establish a reliable improvement. Cache state and the data distribution can affect your observation.&lt;/p&gt;

&lt;p&gt;Your output is a before-and-after comparison with actual measurements. If the plan does not change, that is still useful evidence to investigate. The goal is not to persuade PostgreSQL to validate your first guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring the three pieces back into one explanation
&lt;/h2&gt;

&lt;p&gt;These resources become more useful together because they make you discuss the same app from different directions.&lt;/p&gt;

&lt;p&gt;The service-promise card describes the experience you care about. The retry timeline describes what must remain true when communication fails. The query comparison gives you evidence about one piece of the performance story.&lt;/p&gt;

&lt;p&gt;None of them replaces a full system design. Together, though, they make it harder to hide behind a diagram full of familiar components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ficj82o3erawp6y9knmlj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ficj82o3erawp6y9knmlj.png" alt="A final notebook checklist asks readers to keep a service promise, a retry timeline, and a query comparison, then explain one changeable assumption." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Close the source tabs and explain the app aloud for five minutes. Start with the user action, describe the promise, walk through the lost response, and explain what the query experiment did or did not demonstrate. If a statement depends on an assumption, say the assumption.&lt;/p&gt;

&lt;p&gt;For another round, choose a problem from this &lt;a href="https://prachub.com/questions?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;interview practice collection&lt;/a&gt; and bring just one of these exercises into it. A small, relevant application is more useful than trying to squeeze every chapter into every answer.&lt;/p&gt;

&lt;p&gt;I prefer finishing with three imperfect artifacts I can discuss over thirty pristine bookmarks I have not used. An unanswered question written beside a real example gives the next practice session a useful starting point.&lt;/p&gt;

&lt;p&gt;Save the resources, absolutely, but give yourself something to explain before collecting the next three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
    </item>
    <item>
      <title>Backend Interview Question: Two Buyers Want the Last Seat. Who Gets It?</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Wed, 11 Nov 2026 17:52:28 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/backend-interview-question-two-buyers-want-the-last-seat-who-gets-it-2oge</link>
      <guid>https://dev.to/emilywoodsnyc/backend-interview-question-two-buyers-want-the-last-seat-who-gets-it-2oge</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;A small PostgreSQL example, a deliberately broken timeline, and the follow ups that turn a working demo into a defensible answer.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Both buyers saw the same available seat. Both clicked the button and both received a confirmation, and now somebody has a very awkward email to write.&lt;/p&gt;

&lt;p&gt;Nothing about this requires a million requests per second. Two requests arriving in the wrong order are enough.&lt;/p&gt;

&lt;p&gt;That is why I like this backend practice problem. You can draw an impressive architecture around it, but eventually somebody has to explain which operation is allowed to say that seat A7 belongs to one buyer.&lt;/p&gt;

&lt;p&gt;Let’s work through an original interview style exercise, without pretending it came from a particular company. We control the inventory, use one PostgreSQL database for claims, and start with a single seat for a single event. Payment and expiring holds come later because they add different promises.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fclz4metz5fw0psf9fzkq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fclz4metz5fw0psf9fzkq.png" alt="Two buyers see an available seat on their phones, while a single theatre seat A7 sits between them." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the promise, because available is only an observation
&lt;/h2&gt;

&lt;p&gt;Our first rule is that a seat can have at most one committed owner. We also need the application to avoid telling two different buyers that each owns it.&lt;/p&gt;

&lt;p&gt;Those are related, but they are not identical. A database row can end up with one owner even after the application has sent two contradictory success messages.&lt;/p&gt;

&lt;p&gt;For this exercise, a claim is final once committed. We are not implementing cancellation, seat transfers, or checkout timeouts yet. Buyer identity comes from the authenticated request, and the seat is identified by both event and seat number.&lt;/p&gt;

&lt;p&gt;I prefer making those assumptions visible before reaching for a lock. Otherwise, we can spend ten minutes solving a reservation problem when the interviewer meant a temporary hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the version that looks perfectly reasonable in a demo
&lt;/h2&gt;

&lt;p&gt;Imagine the application reads the row, sees no owner, and then sends a separate update that assigns the buyer. Its update identifies the seat but does not require the owner to remain empty.&lt;/p&gt;

&lt;p&gt;Buyer A reads the empty owner. Before A writes anything, buyer B reads that same empty owner. A assigns the seat to A and receives a success response. B then assigns the seat to B and receives another success response.&lt;/p&gt;

&lt;p&gt;The final row contains B. That does not rescue A’s confirmation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa0puxtvbjulsgn7iccwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa0puxtvbjulsgn7iccwm.png" alt="A broken timeline shows both buyers reading an empty owner before separate writes assign and then overwrite the seat." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wrapping this exact read-then-unconditional-write sequence in transactions is not, by itself, the missing explanation. You still need to explain how competing transactions are prevented from acting on the same earlier observation.&lt;/p&gt;

&lt;p&gt;Disabling the button in the browser cannot coordinate two buyers either. Neither can a lock inside just one application process when requests can land on different instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the condition where ownership changes
&lt;/h2&gt;

&lt;p&gt;Here is a deliberately small schema for the exercise. Each event seat pair has exactly one row, and an empty owner means unclaimed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;seat_claims&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="kt"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;seat_no&lt;/span&gt; &lt;span class="kt"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;buyer_id&lt;/span&gt; &lt;span class="kt"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_no&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;seat_claims&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_no&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'A7'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buyer 101 attempts this conditional update. Buyer 202 runs the same statement with their own authenticated identity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;seat_claims&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;buyer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;seat_no&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'A7'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;buyer_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;RETURNING&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_no&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buyer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the requirement that the seat is unclaimed belongs to the write itself. We have removed the gap between trusting an earlier check and assigning ownership.&lt;/p&gt;

&lt;p&gt;Under PostgreSQL’s Read Committed behavior, competing updates to the same row coordinate through the database. If A holds the row while B attempts the update, B waits. When A commits, B checks its condition against the updated row. The owner is no longer empty, so B changes zero rows. PostgreSQL documents this &lt;a href="https://www.postgresql.org/docs/18/transaction-iso.html" rel="noopener noreferrer"&gt;rechecking behavior for concurrent updates&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfyz49haulxkpa026ikq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfyz49haulxkpa026ikq.png" alt="Two buyers attempt one conditional claim; one commits and owns A7, while the competing update changes zero rows." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One returned row means this statement assigned the seat in its transaction. Zero returned rows means it did not claim the seat. In our fixed fixture the seat exists, so zero means it is already claimed; a real endpoint must also decide how to handle missing or inaccessible seats.&lt;/p&gt;

&lt;p&gt;This is a narrow solution for a narrow rule. Every ownership-changing path must respect the same rule. An administrative endpoint that blindly overwrites buyer_id would undo the guarantee we are trying to offer.&lt;/p&gt;

&lt;p&gt;Also, concurrency safety does not promise fairness. We have not established that the person who clicked first will win. Either buyer can win depending on execution order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the celebration on the other side of COMMIT
&lt;/h2&gt;

&lt;p&gt;Honestly, this is the part I want written beside the code in big letters.&lt;/p&gt;

&lt;p&gt;If the statement runs inside an explicit transaction, receiving a returned row is not the same as committing that transaction. A later error can still cause a rollback. The application must not confirm ownership before it knows the commit succeeded.&lt;/p&gt;

&lt;p&gt;Suppose a booking record must accompany the claim. Write both in one database transaction and roll back together if either operation fails. Our tiny example uses the seat row as the ownership record, so it intentionally leaves that extra table out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmtusmpvk42679t1pnogo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmtusmpvk42679t1pnogo.png" alt="A timeline separates the returned update row, successful commit, and buyer confirmation; rollback leaves the seat unclaimed." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let the commit succeed but lose the response on its way to the client. The buyer retries and the conditional update returns nothing because the seat is already owned. That does not necessarily mean their first attempt failed.&lt;/p&gt;

&lt;p&gt;For a production contract, give the operation a durable identifier scoped to the authenticated buyer, persist its result with the claim, and let a retry recover that same result. Reusing an identifier with different intent needs a defined conflict response. The ownership predicate prevents competing claims; identifying the original operation resolves an ambiguous retry. Our short SQL statement implements the first part, not the entire retry contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the race happen deliberately in two terminals
&lt;/h2&gt;

&lt;p&gt;Do this in a disposable local database with two independent connections, using Read Committed. The sequence below is a reproducible practice exercise with expected outcomes, not a claimed production benchmark.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In connection A, begin a transaction and run the conditional update as buyer 101. Leave the transaction open after it returns A7.&lt;/li&gt;
&lt;li&gt;In connection B, begin another transaction and run the same update as buyer 202. It should wait while A holds the conflicting row lock.&lt;/li&gt;
&lt;li&gt;Commit A. B should finish without returning a row, and you can then commit B. Inspect the stored owner and expect 101.&lt;/li&gt;
&lt;li&gt;Reset the fixture after both transactions finish, then repeat with A rolling back instead. B should now be able to claim A7, and committing B should leave owner 202.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep the waiting transactions short, and clean up both connections if you interrupt the exercise. A lock timeout or another database error is an error path to handle, not evidence that somebody successfully acquired the seat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b6o08pnmq4or7sr53hc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b6o08pnmq4or7sr53hc.png" alt="Four concurrency scenarios cover a committed winner, rollback, a lost response, and independent claims for different seats." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check what the database stores and what each caller is told. Two green HTTP responses are not a correctness test, and one final row is not proof that every earlier response was truthful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the follow-up change the design instead of adding random boxes
&lt;/h2&gt;

&lt;p&gt;An expiring hold changes our state model. Now we need a hold identity, an owner, an expiry rule, and a guarded transition to confirmed. An old timeout task must not release a newer buyer’s hold, so its update needs to identify the exact hold it is expiring.&lt;/p&gt;

&lt;p&gt;Payment introduces another boundary. A database transaction cannot roll back a charge performed by an external payment provider. Avoid holding the seat’s database lock open while waiting for that network call. Design the hold, payment result, confirmation, and recovery rules explicitly, including what happens when payment succeeds after the hold expires.&lt;/p&gt;

&lt;p&gt;For a useful variation, try this &lt;a href="https://prachub.com/interview-questions/design-an-inventory-system-for-a-grocery-delivery-service?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;inventory design problem&lt;/a&gt;. Begin by deciding whether the service actually controls the stock. A reservation in your database cannot stop an unrelated shopper from taking an item off somebody else’s store shelf.&lt;/p&gt;

&lt;p&gt;That is the habit I want this little seat example to build. Name the promise, find the exact write that protects it, and make two callers challenge it at the same time.&lt;/p&gt;

&lt;p&gt;Before moving to your next problem, explain what changes when A rolls back and when A commits but loses the response. If those two stories sound different in your answer, you are practicing something much more useful than memorizing the word transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>database</category>
    </item>
    <item>
      <title>Before You Solve Another Interview Question, Try This 45 Minute Mock</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Wed, 11 Nov 2026 03:37:26 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/before-you-solve-another-interview-question-try-this-45-minute-mock-fjg</link>
      <guid>https://dev.to/emilywoodsnyc/before-you-solve-another-interview-question-try-this-45-minute-mock-fjg</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;One complete attempt, a useful debrief, and a much clearer idea of what to practice tomorrow.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Finishing a practice question can feel wonderfully reassuring, especially when the green checkmark arrives before your coffee gets cold.&lt;/p&gt;

&lt;p&gt;But close the solution tab, start a timer, and explain your decisions out loud, and suddenly you are practicing something a little different.&lt;/p&gt;

&lt;p&gt;Now you have to notice an ambiguous requirement, choose an approach, recover from a wrong turn, and leave enough time to check your work. Nobody has conveniently highlighted the part where your reasoning went sideways.&lt;/p&gt;

&lt;p&gt;That gap interests me much more than the number beside a solved-problems counter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before adding another question to the pile, I prefer one practice session that leaves behind evidence of what actually happened.&lt;/strong&gt; You can run it alone, and you do not need an elaborate setup or a friend who is free at exactly the right time.&lt;/p&gt;

&lt;p&gt;Give yourself 45 minutes for the attempt, then another ten for the review. Those timings are a practice proposal, not a company’s interview format or a pass/fail rubric.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbbka3w7ifni37s394gp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbbka3w7ifni37s394gp.png" alt="A timer and evidence sheet beside a stack of practice cards introduce one honest coding mock." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up a session you cannot accidentally turn into a tutorial
&lt;/h2&gt;

&lt;p&gt;Choose a problem you have not recently solved, in a language you already use. Something modest is fine. An unfamiliar implementation of a familiar idea can reveal more than a famously difficult problem whose solution you remember.&lt;/p&gt;

&lt;p&gt;Open a blank editor and a scratchpad. Decide which tools are allowed before starting, ideally using the rules for your upcoming interview. For an unaided baseline, keep AI assistance and solution tabs closed. If you use a reference or hint, record what it supplied instead of quietly rewriting the story afterward.&lt;/p&gt;

&lt;p&gt;Talking through your decisions helps make the attempt reviewable. A private audio recording is optional; timestamped notes work too. You only need to capture moments such as when you settled the contract, chose an approach, found a bug, or changed your mind.&lt;/p&gt;

&lt;p&gt;Microsoft’s &lt;a href="https://careers.microsoft.com/v2/global/en/hiring-tips/technical-interviewing.html" rel="noopener noreferrer"&gt;technical interview guidance&lt;/a&gt; explicitly discusses clarifying ambiguity, planning a solution, and testing it. So this practice includes the work around the code, too.&lt;/p&gt;

&lt;p&gt;For an accessible practice option, this &lt;a href="https://prachub.com/coding-questions/count-user-sessions-from-logs?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;session-log exercise&lt;/a&gt; asks you to count sessions per user from possibly unsorted event logs. Its full prompt, constraints and examples are publicly readable. You can use it with the timing plan below and review your attempt against its own rules.&lt;/p&gt;

&lt;p&gt;The worked examples below use a separate sign-in/sign-out exercise, which you can attempt directly from this prompt. Given chronological log records of the form &lt;code&gt;user_id timestamp action&lt;/code&gt;, with integer user IDs, integer timestamps in seconds and actions &lt;code&gt;sign-in&lt;/code&gt; or &lt;code&gt;sign-out&lt;/code&gt;, return each user who has at least one completed session lasting at most a nonnegative duration limit, once, in ascending numerical order. Each user can have several sessions, has at most one open session at a time, and every sign-in has a matching sign-out. Events from different users can overlap. Return an empty list when nobody qualifies.&lt;/p&gt;

&lt;p&gt;For a clean attempt, choose either the linked counting exercise or the self-contained sign-in/sign-out exercise and run the mock before reading the worked debrief below. The sign-in/sign-out details in that debrief apply to the self-contained exercise. If you already know the problem you chose, pick a different one rather than timing your memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give each part of the attempt a visible finish line
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89cwvg25bchfthj9xfdj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89cwvg25bchfthj9xfdj.png" alt="Five stages allocate 45 minutes to clarification, planning, implementation, testing, and explaining trade-offs." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Minutes 0–5 should leave you with a contract
&lt;/h2&gt;

&lt;p&gt;Write down the input, output, and a rule that could change the answer. Then predict the result of one tiny example before touching the keyboard.&lt;/p&gt;

&lt;p&gt;For the session exercise, users qualify when &lt;strong&gt;any&lt;/strong&gt; completed session is short enough. That is different from requiring every session to be short. Results must contain each qualifying user once, in ascending numerical order.&lt;/p&gt;

&lt;p&gt;Also notice what the prompt already guarantees. Logs arrive chronologically, and each user’s sign-ins and sign-outs are well formed. You can mention those assumptions without spending your practice time designing a repair system for malformed logs.&lt;/p&gt;

&lt;p&gt;By minute five, another person should be able to read your notes and understand what your function promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minutes 5–12 should leave you with an explainable plan
&lt;/h2&gt;

&lt;p&gt;Describe a straightforward approach first, even if you expect to improve it. Walk through a small input and say what information must survive from one step to the next.&lt;/p&gt;

&lt;p&gt;For this exercise, ask yourself how an overlapping session from a second user affects the information you need to remember. Explain why the state belongs to a particular user rather than to the log as a whole.&lt;/p&gt;

&lt;p&gt;Do not settle for naming a data structure. Explain what one entry means and when it changes. That sentence is often where a fuzzy plan becomes a plan you can implement.&lt;/p&gt;

&lt;p&gt;If you are stuck, write down the exact obstacle. Being unsure how to match events needs a different next step from being unsure how to sort the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minutes 12–30 should leave you with a working path
&lt;/h2&gt;

&lt;p&gt;Implement the approach you can currently defend. Keep the function close to the contract and resist building helpers for situations the prompt explicitly excludes.&lt;/p&gt;

&lt;p&gt;When something breaks, describe the smallest input that exposes it before changing the code. Otherwise, a quick fix can become a little guessing game with very convincing variable names.&lt;/p&gt;

&lt;p&gt;If you need a hint, pause long enough to note the missing idea. Continue afterward, but mark that part of the attempt as assisted. There is useful information in both an independent solution and a solution completed with one well-chosen nudge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minutes 30–40 should leave you with justified tests
&lt;/h2&gt;

&lt;p&gt;Predict expected outputs before running the cases. A program agreeing with your intuition is not much evidence if your intuition made the same mistake as the program.&lt;/p&gt;

&lt;p&gt;Start with the smallest meaningful boundary, then use a case that distinguishes two plausible interpretations. For the session problem, equality with the duration limit matters, and so does a user who has a long session followed by a short one.&lt;/p&gt;

&lt;p&gt;If the implementation is unfinished, test the completed part or trace the intended behavior. Do not erase ten minutes of review just to make the function look finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minutes 40–45 should leave you with an honest explanation
&lt;/h2&gt;

&lt;p&gt;Explain why the approach works, its time and memory costs, and one limitation that comes from the assumptions. Include the cost of producing the required output rather than stopping your analysis at the main loop.&lt;/p&gt;

&lt;p&gt;Finish by stating what remains unverified. That might be a boundary case, an incomplete branch, or a complexity claim you have not worked through yet.&lt;/p&gt;

&lt;p&gt;When the timer ends, preserve the code as it is. You can improve it afterward, but save the original attempt so the review has something real to examine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the first wrong turn, not just the final answer
&lt;/h2&gt;

&lt;p&gt;Here is why I like the session exercise for a debrief. Suppose the limit is five seconds and user 2 has sessions lasting 18 seconds and two seconds.&lt;/p&gt;

&lt;p&gt;That user qualifies because the second session satisfies the rule. If your implementation excludes them, another hour of typing faster will not repair the original misunderstanding.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifcbsv31vx20thocd9v1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifcbsv31vx20thocd9v1.png" alt="A user with an 18-second and a two-second session qualifies under any-session logic, but not every-session logic." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now trace a few other claims independently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A session from time 10 to time 10 qualifies when the limit is zero, because its duration is zero.&lt;/li&gt;
&lt;li&gt;A session lasting exactly five seconds qualifies when the limit is five, so a strict less-than comparison is wrong.&lt;/li&gt;
&lt;li&gt;Two qualifying sessions for the same user still produce one user ID.&lt;/li&gt;
&lt;li&gt;If user 7 qualifies before user 2, the output must still place 2 before 7.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These cases help locate the break. Misreading the rule is a contract problem. Understanding the rule but writing the wrong comparison is an implementation problem. Never checking equality is a testing gap. They can produce similar symptoms, but they deserve different practice.&lt;/p&gt;

&lt;p&gt;For a typical dictionary-and-set solution, scanning n events takes expected O(n) time, while sorting k qualifying IDs adds O(k log k). Storage grows with the users tracked and the qualifying IDs. If your explanation stopped at linear time, the output-order requirement gives you something precise to revisit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replace your verdict with three pieces of evidence
&lt;/h2&gt;

&lt;p&gt;Please do not finish this session by declaring yourself terrible at interviews. That is an enormous conclusion to squeeze out of one function and a timer.&lt;/p&gt;

&lt;p&gt;Use ten minutes to write three short notes instead. Each note should connect an observation to a next action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fileyeb7l3lhv0jbgj5ff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fileyeb7l3lhv0jbgj5ff.png" alt="Vague judgments become concrete observations and targeted practice actions on a notebook review sheet." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Record the moment.&lt;/strong&gt; Perhaps you spent 12 minutes discussing approaches before tracing a single example, or only noticed the output needed sorting after the timer ended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name the skill involved.&lt;/strong&gt; Was the missing piece contract reading, choosing state, implementing a known plan, testing boundaries, or explaining complexity? Treat this as a working diagnosis, not a permanent label.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose a smaller repair.&lt;/strong&gt; If you missed the word any, spend the next session turning requirements into examples and counterexamples. If dictionary operations interrupted your reasoning, practice those operations in your chosen language before another full mock.&lt;/p&gt;

&lt;p&gt;For instance, an illustrative review note could say that at minute 34, a zero-duration test failed because the comparison excluded equality. The next action is to write boundary cases before implementation on an unfamiliar problem.&lt;/p&gt;

&lt;p&gt;That note gives tomorrow a job. Feeling disappointed, unfortunately, does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the next attempt answer one question
&lt;/h2&gt;

&lt;p&gt;Fixing today’s code is useful, but solving it again immediately mostly tells you whether you remember the repair.&lt;/p&gt;

&lt;p&gt;After working on the gap, choose a fresh problem that uses the same skill in a different setting. If contract reading caused trouble, pick a prompt where exactly one, at least one, or all changes the result. If testing was weak, prepare expected outputs before execution and check whether you actually did so.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fawk2ddz0ho16i1qt8jum.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fawk2ddz0ho16i1qt8jum.png" alt="A four-stage loop moves from noticing a misread rule to isolating, repairing, and retesting the skill on a new problem." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep a tiny practice record with the problem, the observed gap, the repair, and the evidence from your next attempt. There is no need to turn your evening into a project-management ceremony.&lt;/p&gt;

&lt;p&gt;Also be fair about what a solo mock can tell you. It cannot reproduce another person’s interruptions or judge every aspect of your communication. A partner can help with those later. What you can inspect alone is whether you made assumptions visible, defended your choices, and checked the behavior you promised.&lt;/p&gt;

&lt;p&gt;I want preparation to leave me with more than a comforting counter. A finished session should make the next one easier to choose.&lt;/p&gt;

&lt;p&gt;So before opening another random question, save one honest attempt and find the first place where your reasoning needed help. Then give that specific gap your attention, because now you actually know what you are practicing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>ai</category>
      <category>interview</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Won’t Start My Next System Design Interview With a Load Balancer</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Wed, 11 Nov 2026 03:37:25 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/i-wont-start-my-next-system-design-interview-with-a-load-balancer-1hcl</link>
      <guid>https://dev.to/emilywoodsnyc/i-wont-start-my-next-system-design-interview-with-a-load-balancer-1hcl</guid>
      <description>&lt;p&gt;&lt;em&gt;First 10 minutes are a chance to make the problem smaller before making the diagram bigger.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xb93mfbv0vsgjyknv5g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xb93mfbv0vsgjyknv5g.png" alt="A pencil waits above an architecture diagram while user, promise and scale cards establish the decisions." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your blank whiteboard can make a load balancer look strangely comforting. You know where to put it, you know what connects to it, and within thirty seconds your answer looks like a system design answer.&lt;/p&gt;

&lt;p&gt;Then someone asks what happens when the user is offline, and suddenly the boxes are less helpful than they looked.&lt;/p&gt;

&lt;p&gt;I love a tidy diagram, but I don’t want it making decisions on my behalf. Before reaching for familiar components, I want a smaller thing settled. What does this system owe the person using it?&lt;/p&gt;

&lt;p&gt;Try a quick exercise with me. Imagine the prompt is to design a notification system. Write down what should happen when someone replies to your post while your phone is offline. When you return an hour later, should that notification still exist?&lt;/p&gt;

&lt;p&gt;If your answer is yes, you have already identified a requirement that a live connection alone cannot satisfy. You need a durable way to recover the missed notification. We have a design decision before drawing our first infrastructure box, which is a lovely place to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick one moment worth designing properly
&lt;/h2&gt;

&lt;p&gt;Notification system can mean an unread bell, an emergency pager, a marketing email service or a mobile push platform. Starting with all four gives you an impressive amount of work and very little agreement about what matters.&lt;/p&gt;

&lt;p&gt;For this practice example, let’s choose an in-app inbox. A reply creates a notification for one recipient. That person can see recent notifications and mark them as read. An offline user can return and retrieve what they missed. Email, SMS and scheduled campaigns stay outside our first pass.&lt;/p&gt;

&lt;p&gt;Those are proposed boundaries, so in an interview I’ll confirm them before treating them as facts. If the interviewer wants emergency alerts, the discussion needs to change immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write one user action and one visible outcome before naming a technology.&lt;/strong&gt; For our example, a person returns to the app and can find the reply notification even though they were disconnected when it arrived.&lt;/p&gt;

&lt;p&gt;This is much more useful than asking whether the system should be scalable. Almost any interviewer can say yes to that, and neither of you learns very much.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjgfvsla4snf846w2217.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjgfvsla4snf846w2217.png" alt="A reply arrives while a user is offline, and remains available when the user reconnects." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Agree on the promise hiding inside success
&lt;/h2&gt;

&lt;p&gt;Now we need to be a little picky about the word delivered. Does it mean the service accepted an event, the inbox contains a notification, an online device received an update, or the person actually opened it?&lt;/p&gt;

&lt;p&gt;Those are different events. Treating them as one vague success flag makes the design sound simpler while leaving the important part unresolved.&lt;/p&gt;

&lt;p&gt;For our first version, propose that an accepted notification is durably recorded before we acknowledge success. An online client should see a new item soon after it becomes available, while a reconnecting client fetches missed items from durable storage. Read state changes when the user takes the relevant action, not merely because the server sent an update.&lt;/p&gt;

&lt;p&gt;We also need an answer for repeated input. If a producer retries the same reply event, should the user see two identical notifications? Assume no, and define an identity such as the event ID together with the recipient ID. Creating the same logical notification twice must resolve to the same stored item.&lt;/p&gt;

&lt;p&gt;Notice that we haven’t promised that every network delivery happens exactly once. We have defined the user-visible outcome and left room to choose the mechanism that protects it.&lt;/p&gt;

&lt;p&gt;My notes at this point can be wonderfully plain. Keep accepted work, recover after disconnection, suppress duplicate inbox items, and keep read state meaningful. Each of those statements will give a later box a job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qp6i774ej2cne1e2c3u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qp6i774ej2cne1e2c3u.png" alt="Accepted, available, live update and read are shown as four distinct events." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for a number only when it changes a decision
&lt;/h2&gt;

&lt;p&gt;Capacity estimates can become a small performance of their own. You multiply a few large numbers, say a storage unit with confidence, and then choose the architecture you were going to choose anyway.&lt;/p&gt;

&lt;p&gt;I prefer asking what the estimate will help us decide before doing the arithmetic.&lt;/p&gt;

&lt;p&gt;Suppose our illustrative workload has 100,000 active users, each receiving ten notifications a day. That gives us one million inbox writes daily, or roughly twelve per second on average. This is a practice assumption, not traffic data from a real company.&lt;/p&gt;

&lt;p&gt;Twelve writes per second tells us surprisingly little about the hardest moment. If one announcement targets all 100,000 users, materializing a separate inbox item for each recipient creates 100,000 writes from one event. Now we have a reason to discuss bursts, background work and whether broadcast messages need a different representation.&lt;/p&gt;

&lt;p&gt;Reads deserve their own estimate too. A badge fetched on every page view can have a different workload from an inbox opened twice a day. Ask about those access patterns rather than deriving every request from the same daily-user number.&lt;/p&gt;

&lt;p&gt;Retention changes the storage discussion. At one million notifications per day, thirty days means thirty million notification records before accounting for indexes, replication or metadata. That does not tell us which database to choose, but it makes the retention assumption worth confirming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An estimate earns its place when it changes what you build, what you defer, or what you need to ask next.&lt;/strong&gt; Otherwise it is arithmetic wearing an interview outfit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmymt3pdqjdfij6wyh4nw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmymt3pdqjdfij6wyh4nw.png" alt="Illustrative workload math separates average writes from a large fan-out event." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Draw one complete path before adding the clever parts
&lt;/h2&gt;

&lt;p&gt;We can finally draw, and honestly, the diagram is easier now because the requirements have started choosing its responsibilities.&lt;/p&gt;

&lt;p&gt;Begin with a producer submitting an event. An ingestion path validates it and records the accepted work durably. A worker creates the recipient’s inbox item using the notification identity to handle retries. An inbox API serves stored notifications and updates read state. A live-update path lets connected clients learn that something changed, while reconnecting clients fetch missed items.&lt;/p&gt;

&lt;p&gt;That is a starting design, not a finished architecture. It gives us a concrete path to challenge rather than a collection of products to admire.&lt;/p&gt;

&lt;p&gt;If the producer times out after the service commits the event, its retry must not create another logical notification. If a worker stops halfway through, pending work must remain discoverable. If the live connection drops, the inbox should still contain the item. Each failure sends us back to one of the promises we already wrote down.&lt;/p&gt;

&lt;p&gt;Be careful around the handoff between a database and a queue. Recording work in one place and separately publishing a message can leave a gap if the process stops between those actions. A &lt;a href="https://github.com/aws-samples/transactional-outbox-pattern" rel="noopener noreferrer"&gt;transactional outbox&lt;/a&gt; is one possible way to make committed pending work discoverable; a durable queue can also be the acceptance boundary in a design built around that contract. The choice needs an explanation, not just another arrow.&lt;/p&gt;

&lt;p&gt;Load balancing may absolutely belong in this system. So may caching, partitioning and a particular messaging technology. I just want to introduce them when there is a responsibility or bottleneck to explain, instead of letting them become the opening sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give your opening a shape you can practise
&lt;/h2&gt;

&lt;p&gt;For a ten-minute rehearsal, use four loose stages. These timings are my practice proposal, not a rule for every interview. Follow the interviewer’s direction when they want to move faster or explore a different area.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use the first two minutes to choose the user moment.&lt;/strong&gt; Describe the action, the expected outcome and the boundaries you are proposing. Confirm them rather than quietly shrinking the assignment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the next two to define success and failure.&lt;/strong&gt; Establish what accepted means, which work must survive, and what duplicates or delays the user can tolerate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend two minutes estimating the work.&lt;/strong&gt; Separate average traffic from bursts, and connect each number to a decision. Keep unanswered assumptions visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the remaining four to draw one complete path.&lt;/strong&gt; Trace an event through it, identify where durable state lives, and choose a failure case to explore next.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want a concrete starting point, use this &lt;a href="https://prachub.com/interview-questions/design-an-in-app-notification-center-with-real-time-updates-and-unread-counts?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;in-app notification-center prompt&lt;/a&gt;. Stop after the opening discussion and first path instead of rushing to reproduce a full solution. Our workload numbers and proposed boundaries above are practice choices to test against the prompt, not facts supplied by it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9aya247uuhffmrrn8mk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9aya247uuhffmrrn8mk.png" alt="A ten-minute rehearsal moves through user moment, success conditions, workload and one complete path." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then close the notes and record a two minute explanation. Listen for places where a component appears without a reason, where an assumption quietly becomes a fact, or where you say reliable without explaining what survives a failure.&lt;/p&gt;

&lt;p&gt;You can repair one of those gaps and repeat the opening without solving an entirely new question. That makes the session small enough to finish and specific enough to learn from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the diagram show decisions you actually made
&lt;/h2&gt;

&lt;p&gt;An opening full of questions can still go nowhere if the answers never affect the design. After each clarification, say what it changes. Offline access creates a recovery requirement. Repeated events require a stable identity. Large fan-out raises a workload question that an average request rate hides.&lt;/p&gt;

&lt;p&gt;That is the thread I want running through my answer. A reader should be able to point at a box and explain which requirement put it there.&lt;/p&gt;

&lt;p&gt;For the next practice session, leave the infrastructure names off the page for a few minutes. Pick the user moment, make the promise concrete, and follow the work.&lt;/p&gt;

&lt;p&gt;When the load balancer finally arrives, it can have a job instead of being there to make the whiteboard feel less empty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>systemdesign</category>
      <category>coding</category>
      <category>ai</category>
    </item>
    <item>
      <title>System Design Interview: Your Queue Is Growing. Will More Workers Actually Help?</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Tue, 10 Nov 2026 18:05:16 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/system-design-interview-your-queue-is-growing-will-more-workers-actually-help-1kbb</link>
      <guid>https://dev.to/emilywoodsnyc/system-design-interview-your-queue-is-growing-will-more-workers-actually-help-1kbb</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;A small backlog calculation that makes scaling, downstream limits, and recovery time much easier to explain.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;You add a queue to the architecture, draw a few workers behind it, and feel the design becoming pleasantly sensible. Requests no longer wait for every background task, and a traffic spike has somewhere to go.&lt;/p&gt;

&lt;p&gt;Then someone asks what happens when jobs arrive faster than the workers can finish them.&lt;/p&gt;

&lt;p&gt;Adding workers sounds like a perfectly reasonable answer. Sometimes it is exactly the right answer. But if every worker is waiting for the same overloaded database, you may have drawn a larger crowd around the same small door.&lt;/p&gt;

&lt;p&gt;I prefer doing a little arithmetic before adding another box to the diagram. It tells us whether the proposed fix can actually close the gap, which is rather helpful when the gap is growing every second.&lt;/p&gt;

&lt;p&gt;Let’s use an original practice scenario with simple numbers. No company interview claim, no imaginary production incident, and no queueing-theory exam hiding around the corner.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfix5n3glt2qrgppkpaz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfix5n3glt2qrgppkpaz.png" alt="More workers feed the same narrow database gate, illustrating why worker count alone cannot remove a downstream bottleneck." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the queue a job and the user a promise
&lt;/h2&gt;

&lt;p&gt;Imagine a service that accepts background jobs to generate customer reports. Once accepted, a job must remain recoverable until it succeeds or reaches an explicit failed state that the user can see.&lt;/p&gt;

&lt;p&gt;Acceptance does not mean the report is ready. The interface returns a job ID, and the user checks its status or receives a completion notification. We also need to agree how long waiting is acceptable, because a successfully queued report can still arrive too late to be useful.&lt;/p&gt;

&lt;p&gt;For the arithmetic, assume equal-cost jobs, steady rates, no retries, no duplicates, and no startup delay. Count unfinished work across the queue and workers, rather than pretending that fetching a message means completing it. These assumptions make the example easy to check; real measurements will be messier.&lt;/p&gt;

&lt;p&gt;Our incoming rate is 1,200 jobs per second. We have 40 workers, and each can complete 20 jobs per second when its dependencies are healthy. The database can support at most 1,000 completed jobs per second for this workload.&lt;/p&gt;

&lt;p&gt;That last number is part of this hypothetical scenario, not a general database limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the gap before choosing the fix
&lt;/h2&gt;

&lt;p&gt;Forty workers completing 20 jobs each gives us 800 completions per second. With 1,200 new jobs arriving, unfinished work grows by 400 jobs every second.&lt;/p&gt;

&lt;p&gt;After five minutes, the additional backlog is 400 multiplied by 300, or &lt;strong&gt;120,000 unfinished jobs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F960zb25afkv98k5ibfzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F960zb25afkv98k5ibfzu.png" alt="An arrival stream of 1200 jobs per second and a completion stream of 800 leave 400 additional unfinished jobs each second, producing 120000 in five minutes." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The queue has done something useful by holding the work. It has not created the capacity to finish it.&lt;/p&gt;

&lt;p&gt;This is why a healthy enqueue endpoint is not enough evidence that the overall service is healthy. Our API can keep accepting jobs quickly while the completion promise gets further away.&lt;/p&gt;

&lt;p&gt;For this simple model, write the relationship as backlog growth per second equals arrivals minus successful completions, while a backlog exists. Keep every rate in the same units and count real completed jobs, not polling calls or processing attempts.&lt;/p&gt;

&lt;p&gt;Already, the discussion has become more specific. We need to close a 400 job per-second gap before we can even begin catching up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Double the workers, then check what they share
&lt;/h2&gt;

&lt;p&gt;Now double the fleet to 80 workers. Their standalone potential is 1,600 jobs per second, which looks comfortably above the arrival rate.&lt;/p&gt;

&lt;p&gt;Unfortunately, every completed report still needs the database, and our assumed database ceiling remains 1,000 jobs per second. Under this simplified model, effective capacity is limited by the smaller of worker capacity and downstream capacity.&lt;/p&gt;

&lt;p&gt;So scaling helps us move from 800 to 1,000 completions per second. That is an improvement, but it still leaves a 200-job-per-second deficit against 1,200 arrivals. The backlog grows more slowly; it does not drain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqgg23dzeq76yp63mwp2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqgg23dzeq76yp63mwp2.png" alt="Doubling workers raises potential throughput from 800 to 1600 jobs per second, but the database caps effective throughput at 1000." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a real service, pushing past a dependency’s useful concurrency can also increase contention or timeouts. The neat ceiling in our example is generous enough to keep that complication out of the calculation.&lt;/p&gt;

&lt;p&gt;Before scaling, inspect where workers spend time. Are they doing CPU work, waiting for database connections, hitting an external rate limit, or sitting behind a hot partition? More workers are a proposal about the limiting resource, so the answer needs evidence about that resource.&lt;/p&gt;

&lt;p&gt;If the database is the limit, possible changes include reducing the work each job asks it to do, changing the access pattern, or increasing its usable capacity. Each needs its own correctness and performance check. A larger worker fleet cannot substitute for that investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching up is a different calculation from keeping up
&lt;/h2&gt;

&lt;p&gt;Suppose we start recovery with the 120,000 unfinished jobs calculated earlier. Traffic now falls to 700 jobs per second, and the scaled system can sustain 1,000 successful completions per second.&lt;/p&gt;

&lt;p&gt;It is tempting to divide 120,000 by 1,000 and declare that we will recover in two minutes. That calculation quietly assumes new work has stopped arriving.&lt;/p&gt;

&lt;p&gt;But 700 of those 1,000 completions are needed just to keep up with new arrivals. Only 300 per second are available to reduce the backlog.&lt;/p&gt;

&lt;p&gt;Our estimated catch-up time is therefore &lt;strong&gt;120,000 divided by 300, or 400 seconds&lt;/strong&gt;. That is six minutes and forty seconds in the constant-rate model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyw1b40033ckuu5r2v5t8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyw1b40033ckuu5r2v5t8.png" alt="Recovery time is calculated from backlog divided by spare completion capacity, yielding 400 seconds when 700 new jobs arrive and 1000 finish per second." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If arrivals stayed at 1,000 per second, there would be no spare capacity to clear the accumulated work. If they remained above 1,000, the backlog would continue growing.&lt;/p&gt;

&lt;p&gt;This estimate describes when the total backlog clears. It is not a prediction of an individual report’s waiting time or a p99 latency promise. Job sizes, scheduling, bursts, and startup delays all affect what particular users experience.&lt;/p&gt;

&lt;p&gt;Try changing one assumption before moving on. If new arrivals fall to 400 per second, spare capacity becomes 600 and the same backlog clears in 200 seconds. I like this follow-up because it checks whether the reasoning survived after the first set of numbers disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what happens before the backlog becomes a broken promise
&lt;/h2&gt;

&lt;p&gt;Once we know the gap, we need a policy for work the system cannot finish on time. That decision belongs beside the capacity calculation, not after an optimistic autoscaling arrow.&lt;/p&gt;

&lt;p&gt;For our report service, a reasonable starting proposal is to limit admission when the estimated delay exceeds the product’s agreed budget. A rejected request must receive a clear response rather than an accepted job ID that will never be honored. Already accepted reports still need a recoverable outcome.&lt;/p&gt;

&lt;p&gt;Some products prefer to keep accepting work and tolerate longer delays. Others allow obsolete work to be cancelled or coalesced. Those choices depend on the contract; throwing away accepted work merely because a graph looks uncomfortable is not a recovery strategy.&lt;/p&gt;

&lt;p&gt;Amazon’s &lt;a href="https://aws.amazon.com/builders-library/avoiding-insurmountable-queue-backlogs/" rel="noopener noreferrer"&gt;discussion of queue backlogs&lt;/a&gt; describes the recovery risk of accumulated work and the importance of measuring message age. It is useful context for why a queue’s durability does not remove its latency trade-off.&lt;/p&gt;

&lt;p&gt;For the report example, I want a small operational picture that answers concrete questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track arrivals and successful completions in jobs per second so the widening or narrowing gap is visible.&lt;/li&gt;
&lt;li&gt;Track the age of the oldest waiting job and end-to-end completion latency so waiting users do not disappear behind a depth chart.&lt;/li&gt;
&lt;li&gt;Track queued, in-flight, and failed work separately so moving messages around does not look like finishing them.&lt;/li&gt;
&lt;li&gt;Track dependency latency and saturation alongside worker utilization so scaling follows the actual constraint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A queue of 1,000 tiny reports and a queue of 1,000 huge reports are not equivalent. Count is useful, but the amount of work and the age of that work matter too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring retries back into the picture carefully
&lt;/h2&gt;

&lt;p&gt;We excluded retries from the arithmetic because they deserve their own line in the answer. A failed attempt still uses resources, and a retry can use them again without adding a new user job.&lt;/p&gt;

&lt;p&gt;Suppose the database starts timing out and workers immediately retry. Attempt traffic can rise even while successful completion throughput falls. Looking only at how busy the workers are would be terribly flattering to the wrong metric.&lt;/p&gt;

&lt;p&gt;Bound retry attempts or elapsed retry time, apply suitable backoff, and keep failures visible. If a job is moved to a dead-letter queue for investigation, it has not become a successfully generated report. Define who owns recovery and what the user sees.&lt;/p&gt;

&lt;p&gt;Also separate throughput from correctness. If delivery can happen more than once, the report’s externally visible effects still need duplicate protection. Processing the same job twice does not create two units of useful customer progress.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1hwzahowdkri66uw0g9g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1hwzahowdkri66uw0g9g.png" alt="A notebook checklist covers successful completions, downstream limits, message age, retry budgets, and estimated catch-up time before adding workers." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Practise the explanation without hiding behind the diagram
&lt;/h2&gt;

&lt;p&gt;Take one background-processing problem from this &lt;a href="https://prachub.com/questions?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;interview question collection&lt;/a&gt;, then add your own explicit arrival rate, worker rate, and dependency ceiling. Keep your assumptions separate from the original prompt.&lt;/p&gt;

&lt;p&gt;Explain how fast unfinished work grows, whether scaling changes the limiting resource, and how long recovery takes while new work continues to arrive. Finish by describing what happens to accepted jobs when the waiting-time budget is exceeded.&lt;/p&gt;

&lt;p&gt;For our scenario, the answer is concrete. Forty workers finish 800 jobs per second, so five minutes at 1,200 arrivals creates 120,000 unfinished jobs. Doubling workers helps, but the database caps completions at 1,000. When arrivals fall to 700, the backlog takes 400 seconds to clear under our assumptions.&lt;/p&gt;

&lt;p&gt;That is a much more defensible design conversation than adding workers and hoping the queue feels better about it.&lt;/p&gt;

&lt;p&gt;I still want the queue in the architecture when it serves the contract. I just want the recovery calculation beside it, because storing a promise and keeping it are two different pieces of engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
    </item>
    <item>
      <title>I Don’t Trust AI-Written Tests Until They Fail for the Right Reason</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Tue, 10 Nov 2026 07:08:38 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/i-dont-trust-ai-written-tests-until-they-fail-for-the-right-reason-1pp8</link>
      <guid>https://dev.to/emilywoodsnyc/i-dont-trust-ai-written-tests-until-they-fail-for-the-right-reason-1pp8</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;A small way to check whether your tests protect the requirement or simply agree with the code in front of them.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Your coding assistant finishes the function, writes the tests, runs them, and hands you a lovely green result. Honestly, that is a very tempting moment to close the tab and feel productive.&lt;/p&gt;

&lt;p&gt;But I want to know what would make those tests turn red.&lt;/p&gt;

&lt;p&gt;If the implementation misunderstood the requirement and the tests repeated that misunderstanding, they can agree with each other beautifully. Meanwhile, the user still gets the wrong result. A second file is not automatically a second opinion.&lt;/p&gt;

&lt;p&gt;This is not a reason to stop asking AI for tests. It is a reason to give those tests something specific to disagree with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before trusting a passing suite, deliberately introduce one small, meaningful mistake and check whether the right assertion catches it.&lt;/strong&gt; Then restore the correct code and run the tests again.&lt;/p&gt;

&lt;p&gt;Let’s make that less abstract with a shipping calculation small enough to check without trusting anyone’s confident explanation, including mine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgautvfk3o8zjdxb2peh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgautvfk3o8zjdxb2peh.png" alt="A developer inspects a green test report beside a boundary bug that the report missed." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the rule before reading the implementation
&lt;/h2&gt;

&lt;p&gt;For this original example, imagine a shop with a deliberately simple shipping policy. Orders below 5,000 cents pay 499 cents for shipping. Orders at or above 5,000 cents receive free shipping. Negative subtotals are invalid and must raise ValueError.&lt;/p&gt;

&lt;p&gt;Assume the input is already an integer number of cents. Currency conversion, discounts, taxes, and input parsing are outside this little function’s job.&lt;/p&gt;

&lt;p&gt;Notice that the rule gives us an expected answer at exactly 5,000 cents. Free shipping starts there, not one cent later.&lt;/p&gt;

&lt;p&gt;I prefer writing that down before looking at the generated code because it keeps the code from quietly becoming the specification. If the policy is unclear, resolve the ambiguity first; no testing trick can decide what the business meant.&lt;/p&gt;

&lt;p&gt;Here is a correct implementation for the stated rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shipping_cents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal_cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;subtotal_cents&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'subtotal must be nonnegative'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;subtotal_cents&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;499&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine the proposed tests check that a 1,000-cent order pays 499 cents and a 6,000-cent order pays zero. Both pass, as they should.&lt;/p&gt;

&lt;p&gt;Those tests are useful, but they leave a particularly interesting customer standing between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change one character and watch what stays green
&lt;/h2&gt;

&lt;p&gt;In a disposable local copy, change the final comparison from &lt;code&gt;&amp;gt;= 5000&lt;/code&gt; to &lt;code&gt;&amp;gt; 5000&lt;/code&gt;. Leave everything else alone.&lt;/p&gt;

&lt;p&gt;The 1,000 cent test still passes. So does the 6,000-cent test. Neither input asks the function what happens exactly at the threshold.&lt;/p&gt;

&lt;p&gt;But a customer spending 5,000 cents now pays 499 cents when the policy promises free shipping. We introduced a real behavior change, and the original pair of tests did not object.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4yhftgqtvqijxen7562l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4yhftgqtvqijxen7562l.png" alt="A three-point number line shows the correct shipping fee below, at, and above 5000 cents, and highlights the buggy fee at the boundary." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the part I find useful. We have moved from vaguely wanting better tests to naming an exact mistake the tests currently allow.&lt;/p&gt;

&lt;p&gt;Add an assertion whose expected result comes from the policy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;shipping_cents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it while the incorrect comparison is still present. It should fail because the returned fee is 499 instead of zero. Then restore &lt;code&gt;&amp;gt;=&lt;/code&gt; and confirm that the same assertion passes.&lt;/p&gt;

&lt;p&gt;A syntax error, broken import, or missing dependency would also produce a red run, but none of those would show that our boundary assertion caught the shipping bug. Read the failure, not just its color.&lt;/p&gt;

&lt;h2&gt;
  
  
  One caught bug is evidence, not a certificate
&lt;/h2&gt;

&lt;p&gt;Now try a different intentional mistake, again starting from the restored correct implementation. Remove the negative-subtotal guard.&lt;/p&gt;

&lt;p&gt;The original two tests still pass. Even the new 5,000-cent test passes. A boundary test for free shipping does not automatically protect invalid-input behavior.&lt;/p&gt;

&lt;p&gt;So add a separate check that a negative subtotal raises the promised exception. Here is a compact version using Python’s standard test library, alongside the nearby valid values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unittest&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShippingTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_fees&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;499&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;499&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shipping_cents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_negative_subtotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;shipping_cents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put this below the function in a file named &lt;code&gt;test_shipping.py&lt;/code&gt;, then run &lt;code&gt;python -m unittest -v test_shipping&lt;/code&gt;. Keep the function and test class at the top level of the file.&lt;/p&gt;

&lt;p&gt;The expected values are intentionally written out. Calculating them with another copy of the production comparison would risk reproducing the same mistake inside the test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuc7k2z6dqiauwicwj6z2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuc7k2z6dqiauwicwj6z2.png" alt="A comparison table shows which intentional mistakes the original tests and the expanded checks detect." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I checked these outcomes against the original and both altered versions. The original pair of tests misses both mistakes. The expanded checks catch the threshold change and the removed guard, while passing against the correct version.&lt;/p&gt;

&lt;p&gt;That is a result for this tiny fixture, not a claim about every AI assistant or a guarantee that the function has no other problems. Different mistakes need different evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the assistant a review task with an observable finish
&lt;/h2&gt;

&lt;p&gt;An instruction to add comprehensive tests sounds reassuring, but comprehensive is doing an awful lot of work there.&lt;/p&gt;

&lt;p&gt;Try a more concrete brief instead. Give the assistant the requirement, ask it to list expected outcomes before inspecting the implementation, and ask which plausible incorrect implementations each test would reject.&lt;/p&gt;

&lt;p&gt;For the shipping example, the review brief can be this small.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Derive the expected fees at 4,999, 5,000, and 5,001 cents from the written policy.&lt;/li&gt;
&lt;li&gt;Check that a negative subtotal raises ValueError without depending on the exact wording of the message.&lt;/li&gt;
&lt;li&gt;Change only the threshold comparison and identify the assertion that fails because of it.&lt;/li&gt;
&lt;li&gt;Restore the function, remove only the negative-input guard, and identify the exception check that fails.&lt;/li&gt;
&lt;li&gt;Restore the correct version again and show the final passing test output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You still need to inspect the proposed expectations and the actual result. An assistant saying it verified the change is not the same as seeing which command ran, which test failed, and why.&lt;/p&gt;

&lt;p&gt;For a bug fix, this can be even simpler. Run the regression test against the known buggy version and the fixed version. It should expose the intended bug in the first and pass in the second. Make sure it fails for the behavior you meant to fix, rather than for an unrelated incompatibility between versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the experiment small enough to understand
&lt;/h2&gt;

&lt;p&gt;This approach is closely related to mutation testing, where tools deliberately alter code and check whether tests detect the changes. &lt;a href="https://pitest.org/quickstart/basic_concepts/" rel="noopener noreferrer"&gt;PIT’s explanation&lt;/a&gt; also makes an important distinction between a detected mutation, a surviving mutation, and code that no test reached.&lt;/p&gt;

&lt;p&gt;Some mutations do not change the behavior that matters, so a survivor is a reason to investigate rather than an automatic demand for another test. Chasing a perfect score can become a very elaborate way to avoid thinking about the requirement.&lt;/p&gt;

&lt;p&gt;You do not need to install a mutation-testing tool to try the manual version on one small function. Tooling becomes more useful when you want to repeat the process across more code; the small example is about learning what evidence to look for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbh6wli06mfkem23y566o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbh6wli06mfkem23y566o.png" alt="A local practice loop moves from a written rule to a passing baseline, one intentional fault, a meaningful failure, and a restored passing result." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep each fault isolated. Start from a passing baseline, make one change, inspect the result, and restore the original before trying the next one. Use a local practice copy or a disposable branch so an intentionally broken version cannot slip into your actual work.&lt;/p&gt;

&lt;p&gt;For a first session, choose one small function with a clear output. Check a boundary, an invalid input, and one meaningful alternative result. Stop once you can explain what each assertion protects; a growing test count is not the goal.&lt;/p&gt;

&lt;p&gt;If you need a fresh starting point, choose a small problem from this &lt;a href="https://prachub.com/questions?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;practice question collection&lt;/a&gt; and write the expected outcomes before opening a solution. Then make one plausible mistake and see whether your own checks notice it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leave with a review note you can defend
&lt;/h2&gt;

&lt;p&gt;For this example, the useful summary is that the original checks missed the exact threshold and negative-input behavior. The added assertions caught those two deliberate faults, and the restored implementation passed them.&lt;/p&gt;

&lt;p&gt;That says considerably more than tests added, all green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7f26i17mnz37f8yj665a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7f26i17mnz37f8yj665a.png" alt="A short review note records the requirement, a deliberate fault, the detecting test, the restored result, and the remaining limits." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In an interview, this gives you a concrete way to explain your verification. During a code review, it gives the next person a reason to trust the specific behavior you checked. Neither requires pretending that a handful of tests proves the entire system correct.&lt;/p&gt;

&lt;p&gt;I still want AI to help with the repetitive parts. I just want the final green result to mean more than the implementation and its tests agreeing with each other.&lt;/p&gt;

&lt;p&gt;Give the tests a believable mistake to catch, and make them earn a little of that confidence.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
    </item>
    <item>
      <title>SQL Interview Question: MAX(created_at) Doesn’t Return the Latest Order</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Tue, 10 Nov 2026 07:08:37 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/sql-interview-question-maxcreatedat-doesnt-return-the-latest-order-1ocg</link>
      <guid>https://dev.to/emilywoodsnyc/sql-interview-question-maxcreatedat-doesnt-return-the-latest-order-1ocg</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;Finding the newest timestamp is easy. Keeping the right row, handling ties, and putting the filter in the right place takes a little more care.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Someone asks for the latest order for each customer, and your brain happily reaches for GROUP BY and MAX.&lt;/p&gt;

&lt;p&gt;Honestly, fair enough. The question practically waves a little flag at an aggregate function.&lt;/p&gt;

&lt;p&gt;Then the interviewer asks for the order ID, its status, and the amount. Two orders have the same timestamp, and one customer’s newest order is cancelled. Suddenly, finding the latest time is only a small part of the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I prefer treating this as a row-selection problem before treating it as a SQL problem.&lt;/strong&gt; We need to decide which complete record wins, not collect impressive-looking values from different records.&lt;/p&gt;

&lt;p&gt;Let’s work through a small original example, then change one sentence in the requirement and watch the answer change with it. There is no giant dataset to hide behind here, which is rather convenient for catching our own assumptions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6re9937tvyy8y9macg24.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6re9937tvyy8y9macg24.png" alt="A timestamp card and a full order receipt show why finding a maximum time does not identify the complete record." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Give yourself five rows that refuse to cooperate
&lt;/h2&gt;

&lt;p&gt;Our orders table contains &lt;code&gt;customer_id&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;total_cents&lt;/code&gt;. Each order has a unique ID. Customer IDs, timestamps, and statuses are non-null, and all times shown below are on the same date.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwq1ew1ap7igggk441bz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwq1ew1ap7igggk441bz.png" alt="Five example orders include a timestamp tie, a cancelled latest order, and a customer with only a cancelled order." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Customer A has three orders. Order 11 was paid at 09:00 for 9,000 cents. Orders 12 and 13 both arrived at 10:00; order 12 is paid for 2,000 cents, while order 13 is cancelled for 3,000 cents.&lt;/p&gt;

&lt;p&gt;Customer B has one paid order, ID 21. Customer C has one cancelled order, ID 31.&lt;/p&gt;

&lt;p&gt;For the first requirement, return exactly one latest order for every customer represented in this table, regardless of status. If timestamps tie, choose the larger order ID. That tie-breaker is an agreed rule for this exercise, not a claim that a larger ID always represents a later real-world event.&lt;/p&gt;

&lt;p&gt;Before writing anything, the expected winning IDs are therefore &lt;strong&gt;13, 21, and 31&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Having that answer on paper makes the query much easier to judge. A plausible result is not enough; we already know which rows the contract requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  MAX gives you a value, not the rest of its row
&lt;/h2&gt;

&lt;p&gt;This query correctly answers which timestamp is greatest for each customer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;latest_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not answer which complete order to return. For customer A, two rows share that maximum timestamp.&lt;/p&gt;

&lt;p&gt;Joining the result back to orders on customer and timestamp can return both. That may be correct if the request is to return every order tied for latest, but it does not satisfy our exactly-one-row rule yet.&lt;/p&gt;

&lt;p&gt;Another tempting repair is to add &lt;code&gt;MAX(total_cents)&lt;/code&gt; to the grouped query. Please trace that against customer A before trusting it. You get 10:00 and 9,000 cents, even though no order in the input contains that combination.&lt;/p&gt;

&lt;p&gt;Each maximum was calculated independently. The result has quietly assembled a record that never existed, which is quite an achievement for such a short query.&lt;/p&gt;

&lt;p&gt;Adding ordinary non-grouped columns is not a reliable solution either. PostgreSQL rejects this form when those columns are not functionally determined by the grouped columns. We need an explicit rule for selecting a row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rank the complete records, then keep the winner
&lt;/h2&gt;

&lt;p&gt;Use a window function to number each customer’s orders. The first ordering field defines latest; the second resolves a tie.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="nb"&gt;ROW_NUMBER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;OVER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
         &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;rn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For customer A, order 13 receives row number one, order 12 receives two, and order 11 receives three. The other customers each have one row, so their numbering starts at one too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwoxavjpb2tmtb1z3d8fn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwoxavjpb2tmtb1z3d8fn.png" alt="Customer A’s complete rows are ranked by timestamp descending and then ID descending, with order 13 selected." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All the selected fields now come from the same record. We have not asked the database to guess which status belongs beside an aggregate timestamp.&lt;/p&gt;

&lt;p&gt;PostgreSQL’s &lt;a href="https://www.postgresql.org/docs/current/tutorial-window.html" rel="noopener noreferrer"&gt;window-function tutorial&lt;/a&gt; explains that ROW_NUMBER numbers rows within a partition and that tied rows have an unspecified numbering order. That is why our unique ID belongs in the ordering clause when exactly one deterministic winner is required.&lt;/p&gt;

&lt;p&gt;Also notice the final &lt;code&gt;ORDER BY customer_id&lt;/code&gt;. Ordering inside the window decides how to assign row numbers; it does not promise the presentation order of the final result.&lt;/p&gt;

&lt;p&gt;If the requirement changes to returning every row tied at the latest timestamp, the ranking rule changes too. A rank based on &lt;code&gt;created_at&lt;/code&gt; alone can preserve those ties. Adding a unique ID to that ranking would separate the very ties you were asked to retain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving one filter changes the question
&lt;/h2&gt;

&lt;p&gt;Now suppose the request becomes latest paid order per customer.&lt;/p&gt;

&lt;p&gt;We should exclude cancelled rows before ranking, so the eligible population contains only paid orders. In the previous query, add &lt;code&gt;WHERE status = 'paid'&lt;/code&gt; immediately after &lt;code&gt;FROM orders&lt;/code&gt; inside the CTE.&lt;/p&gt;

&lt;p&gt;Customer A now returns order 12. Customer B still returns order 21. Customer C has no paid order, so C has no result under this version of the contract.&lt;/p&gt;

&lt;p&gt;But here is a different request that sounds dangerously similar. Return each customer’s latest order, only when that latest order is paid.&lt;/p&gt;

&lt;p&gt;This time, rank all orders first. Then use &lt;code&gt;WHERE rn = 1 AND status = 'paid'&lt;/code&gt; in the outer query. Customer A disappears because A’s winning row is cancelled. Only customer B remains.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jrbkc3walh0m6r8ewkk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jrbkc3walh0m6r8ewkk.png" alt="Filtering paid rows before ranking returns A’s order 12, while ranking first and checking paid afterward excludes A." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both queries run. Neither needs to throw an error. They answer different questions because one filter determines who may compete, while the other checks the winner after the competition.&lt;/p&gt;

&lt;p&gt;That is the sentence I want to be able to explain in an interview. Memorizing the window-function syntax helps, but it cannot decide which population the business meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the missing customer when the requirement asks for them
&lt;/h2&gt;

&lt;p&gt;So far, our population comes from orders. A customer with no orders is invisible, and a customer with no eligible paid order disappears when we filter before ranking.&lt;/p&gt;

&lt;p&gt;If the output must contain every customer, start from a separate customers table and left join the selected order onto it. Keep &lt;code&gt;rn = 1&lt;/code&gt; in the join condition or in a prefiltered winners relation.&lt;/p&gt;

&lt;p&gt;Putting &lt;code&gt;WHERE ranked.rn = 1&lt;/code&gt; after that left join removes the null-extended rows, so customers without a match disappear again. That would undo the inclusion rule you were trying to implement.&lt;/p&gt;

&lt;p&gt;Decide how to represent the absence of an order. Null order fields mean no match; a made-up zero amount would blur that distinction with a real zero-value order.&lt;/p&gt;

&lt;p&gt;If timestamps can be null in the actual dataset, define their meaning before removing our non-null assumption. Unknown time is not automatically the oldest or newest event, and default null ordering differs across databases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the counterexamples do the reviewing
&lt;/h2&gt;

&lt;p&gt;The quickest useful check is to compare the exact selected IDs and their associated values, not just the number of output rows.&lt;/p&gt;

&lt;p&gt;For this dataset, the three versions should produce these results.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latest order regardless of status returns A → 13, B → 21, C → 31.&lt;/li&gt;
&lt;li&gt;Latest paid order returns A → 12 and B → 21.&lt;/li&gt;
&lt;li&gt;Latest order, only if paid, returns B → 21.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then add a customer with no orders, create another timestamp tie, and change which tied row is paid. If your query still matches the written rule, you have better evidence than a clean run on a perfectly polite dataset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxo4srz01jwdfpz19hzud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxo4srz01jwdfpz19hzud.png" alt="A notebook checklist asks about tie policy, eligible rows, missing customers, and whether selected fields belong to one record." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For production data, inspect the execution plan rather than promising that a window function is automatically cheap. An index beginning with &lt;code&gt;(customer_id, created_at DESC, id DESC)&lt;/code&gt; may help the ordering access path, but filters, row counts, selected columns, and the database planner still matter. This all-customer query may process many rows even when it returns few.&lt;/p&gt;

&lt;p&gt;For a useful next variation, try this &lt;a href="https://prachub.com/coding-questions/find-top-paid-employee-per-department-2?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;salary-history and top-earner exercise&lt;/a&gt;. It asks you to identify the latest employee-department record before choosing the highest-paid employee in each department. That adds a second selection stage, so getting the first population right becomes even more important.&lt;/p&gt;

&lt;p&gt;I like leaving this kind of practice with a small question I can reuse. Am I calculating a property of a group, or selecting a particular member of that group?&lt;/p&gt;

&lt;p&gt;Once that distinction is clear, the query becomes much easier to explain. More importantly, every timestamp, status, and amount in the answer can point back to the row that actually earned its place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>database</category>
    </item>
    <item>
      <title>Backend Interview Question: Why Does Page 2 Repeat an Order From Page 1?</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Tue, 10 Nov 2026 07:08:36 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/backend-interview-question-why-does-page-2-repeat-an-order-from-page-1-3pmp</link>
      <guid>https://dev.to/emilywoodsnyc/backend-interview-question-why-does-page-2-repeat-an-order-from-page-1-3pmp</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;One new order can break an apparently correct endpoint. Here is how to trace the bug, write a cursor query, and explain the guarantee honestly.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;You open an orders page, read three rows, and click Next. One of those orders appears again.&lt;/p&gt;

&lt;p&gt;Naturally, you suspect the frontend. Maybe the component appended the wrong response, or perhaps a retry slipped through. Those are reasonable possibilities, but imagine the network response itself contains the repeated order.&lt;/p&gt;

&lt;p&gt;Now the backend gets its little moment of attention.&lt;/p&gt;

&lt;p&gt;Both requests succeeded. Both queries returned three rows. Nobody inserted a duplicate order into the database, and yet the customer is looking at one twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the kind of interview problem I like because the SQL can look completely reasonable while the behavior is wrong.&lt;/strong&gt; The interesting question is what page two means when the data changes after page one.&lt;/p&gt;

&lt;p&gt;Let’s use an original practice scenario with a tiny order history, then build an answer that handles the follow-ups without pretending a cursor freezes time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlnr4pqg7qcuh9ugleiv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlnr4pqg7qcuh9ugleiv.png" alt="Two pages contain order 104, showing a duplicate caused by a shifting pagination boundary." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First, make the bug small enough to see
&lt;/h2&gt;

&lt;p&gt;Suppose we list a customer’s orders newest first, three at a time. Initially, their IDs appear in this order.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;106, 105, 104, 103, 102, 101&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For this first illustration, larger IDs happen to belong to newer orders. We will use an explicit timestamp and ID together in the actual query.&lt;/p&gt;

&lt;p&gt;Page one returns &lt;code&gt;106, 105, 104&lt;/code&gt;. The next request uses &lt;code&gt;LIMIT 3 OFFSET 3&lt;/code&gt;, which means skip three rows in the result available to that request, then return three.&lt;/p&gt;

&lt;p&gt;Before that request runs, a new order arrives at the front.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;107, 106, 105, 104, 103, 102, 101&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Skipping three now skips &lt;code&gt;107, 106, 105&lt;/code&gt;. Page two returns &lt;code&gt;104, 103, 102&lt;/code&gt;, so order 104 makes another appearance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshaq7xhs9kjdib0hpptz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshaq7xhs9kjdib0hpptz.png" alt="Inserting order 107 before the next offset query shifts order 104 onto the second page." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The database followed the instruction correctly. We asked it to skip positions in a changing list, not continue after the order the customer had last seen.&lt;/p&gt;

&lt;p&gt;Deletion produces the opposite surprise. Start again with the original six rows and the same first page. If order 106 disappears before the next request, skipping three rows in the remaining list skips &lt;code&gt;105, 104, 103&lt;/code&gt;. Order 103 never reaches the customer.&lt;/p&gt;

&lt;p&gt;Deduplicating responses in the browser can hide the repeated order, but it does not recover that missing one. Before adding a client-side patch, find out which guarantee the API is supposed to provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stable order fixes one problem, not both
&lt;/h2&gt;

&lt;p&gt;Every page needs a predictable order. Sorting only by creation time leaves a tie whenever two orders share that timestamp.&lt;/p&gt;

&lt;p&gt;Use a unique tie-breaker, such as &lt;code&gt;ORDER BY created_at DESC, id DESC&lt;/code&gt;, and define both fields as non-null. For this example, creation time and ID are immutable, and ID is unique.&lt;/p&gt;

&lt;p&gt;PostgreSQL’s &lt;a href="https://www.postgresql.org/docs/18/queries-limit.html" rel="noopener noreferrer"&gt;LIMIT and OFFSET documentation&lt;/a&gt; explains why a predictable ordering matters and notes that skipped rows still have to be computed. Deep offsets can therefore add work as well as create awkward navigation behavior.&lt;/p&gt;

&lt;p&gt;However, even a perfectly deterministic order does not prevent rows from moving to different positions after an insertion. The repeat we just traced survives the tie-breaker because the list itself changed.&lt;/p&gt;

&lt;p&gt;I like separating those two issues in an interview. We need an unambiguous order, and we need a continuation rule that does not depend on how many rows happen to precede us now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue after the last row you actually returned
&lt;/h2&gt;

&lt;p&gt;For forward browsing, save the ordering values of the last returned row. If that row has creation time 10:00 and ID 104, the continuation boundary contains both values.&lt;/p&gt;

&lt;p&gt;The next page asks for rows strictly after that boundary in our descending order. That means an earlier timestamp, or the same timestamp with a smaller ID.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g122e3rb5t1merm9to2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g122e3rb5t1merm9to2.png" alt="The timestamp and unique ID together identify a boundary, including rows tied at the same timestamp." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a PostgreSQL query using bound parameters. &lt;code&gt;$1&lt;/code&gt; is the authenticated customer's ID, &lt;code&gt;$2&lt;/code&gt; and &lt;code&gt;$3&lt;/code&gt; are the decoded cursor timestamp and ID, and &lt;code&gt;$4&lt;/code&gt; is the server-validated page size plus one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_cents&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="nv"&gt;$4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tuple comparison matches the descending ordering because both columns use the same direction. If you change the sort directions or allow nulls, revisit the predicate rather than copying this one unchanged.&lt;/p&gt;

&lt;p&gt;For the first page, omit the boundary condition. With a page size of three, fetch up to four rows. Return the first three; the extra row tells you whether another result existed at that moment.&lt;/p&gt;

&lt;p&gt;Build the next cursor from the &lt;strong&gt;last row returned&lt;/strong&gt;, not from the extra row you withheld. Otherwise, the next request skips a row before the customer has ever seen it. A later request may still be empty if data was deleted meanwhile, so &lt;code&gt;has_more&lt;/code&gt; is not a reservation of future results.&lt;/p&gt;

&lt;p&gt;In our insertion example, a new order before the saved boundary no longer changes which older orders follow it. Page two continues with &lt;code&gt;103, 102, 101&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, deleting the boundary row does not have to break navigation. The cursor holds its ordering values, so the query can compare against them without finding that row again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the cursor a continuation token, not a permission slip
&lt;/h2&gt;

&lt;p&gt;The token needs enough information to preserve the traversal. Include a format version, the timestamp at its original precision, the ID, and the relevant filter and sort identity. Treat large IDs carefully when encoding them for clients that cannot represent every integer exactly.&lt;/p&gt;

&lt;p&gt;If the customer changes a filter, start a new traversal or reject a cursor from the old one. Continuing a paid-orders query with a cursor created for all orders is a different contract, even if the SQL still runs.&lt;/p&gt;

&lt;p&gt;An opaque, integrity-protected token can prevent clients from casually changing its contents. Base64 alone is only an encoding. Regardless of the token format, derive the customer scope from authentication and apply authorization on every request; do not trust a customer ID inside a cursor as access permission.&lt;/p&gt;

&lt;p&gt;For this query, a candidate B-tree index is &lt;code&gt;(customer_id, created_at DESC, id DESC)&lt;/code&gt;. It puts the customer restriction before the ordered boundary, allowing an appropriate plan to seek near the cursor instead of walking through a large offset.&lt;/p&gt;

&lt;p&gt;That is a design candidate, not a promise that every production query will be fast. Check the query plan and representative data, especially if extra filters discard many rows. Selecting wide columns or adding joins changes the work too.&lt;/p&gt;

&lt;p&gt;Offset pagination still has a place when the dataset is small or stable and jumping directly to a numbered page matters. A forward cursor does not offer cheap arbitrary page jumps. Choose the navigation behavior deliberately instead of treating one technique as universally superior.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cursor cannot promise a snapshot it does not have
&lt;/h2&gt;

&lt;p&gt;Here comes the follow-up that makes the answer more useful.&lt;/p&gt;

&lt;p&gt;Suppose the product team now wants a complete export of every order that existed when the user started. No additions, no disappearances, and values as they were at that moment.&lt;/p&gt;

&lt;p&gt;Our live cursor does not provide that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ovtci6smz41m8sikm9m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ovtci6smz41m8sikm9m.png" alt="Live browsing permits changes, while a frozen export needs a defined snapshot or materialized result." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With immutable sort keys and fixed filters, keyset pagination avoids the position-shift duplicate we traced. But a newly inserted, backdated row behind the cursor can still appear later. A new row ahead of the cursor will not appear during the remaining forward traversal. Deleted rows can disappear.&lt;/p&gt;

&lt;p&gt;If you instead sort by a mutable field such as last-updated time, an existing row can cross the boundary. Depending on the direction of that move, it may be missed or returned again.&lt;/p&gt;

&lt;p&gt;For a fixed export, define a snapshot mechanism or materialize the result the job will export. Saving only IDs preserves membership, not necessarily the original values; a point-in-time export needs those values or a way to retrieve their historical versions too.&lt;/p&gt;

&lt;p&gt;A start-time cutoff can exclude some later inserts, but it is not a complete snapshot when timestamps can be backdated, rows can be deleted, or values can change. Long-lived database snapshots also have operational costs, so avoid holding a transaction open simply because a human might click Next eventually.&lt;/p&gt;

&lt;p&gt;This is why I prefer explaining the browsing promise before choosing a pagination strategy. A live order history and a reconciliation export can look almost identical on a screen while requiring very different guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the boundary instead of staring at the happy path
&lt;/h2&gt;

&lt;p&gt;Start with six rows, fetch a page, and save its cursor. Then change the data before asking for the next page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftc5z1qpkj4vyt0wz7cmi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftc5z1qpkj4vyt0wz7cmi.png" alt="Four tests cover newer inserts, tied timestamps, deletion before the boundary, and changed filters." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Insert a newer row and confirm that the original first-page rows do not repeat during forward traversal.&lt;/li&gt;
&lt;li&gt;Give multiple rows the same timestamp and confirm that the ID tie-breaker returns each eligible row in the correct order.&lt;/li&gt;
&lt;li&gt;Delete a row before the boundary, then delete the boundary row itself in a separate test. Neither case should require guessing a new offset.&lt;/li&gt;
&lt;li&gt;Change the filter and verify that the API resets the traversal or rejects the mismatched cursor according to its contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also test an empty result and an exact page-size result. The latter should not advertise another page merely because you returned three rows; that is why we fetched the extra one.&lt;/p&gt;

&lt;p&gt;For practice, extend this &lt;a href="https://prachub.com/interview-questions/design-paginated-list-ui-and-pagination-api?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;paginated list and API exercise&lt;/a&gt; with the same tiny dataset. Define what its second page promises while you test insertion, deletion, and ties, then add loading states and backward navigation. This walkthrough covers the forward-query contract; the full exercise gives you room to design the surrounding UI too.&lt;/p&gt;

&lt;p&gt;My goal is to leave the conversation with an explanation of what the user will observe, not just a query containing the word cursor somewhere nearby.&lt;/p&gt;

&lt;p&gt;Once you can trace that repeated order all the way from the data change to the response, pagination stops being a small UI detail. It becomes a clear, testable agreement about how someone moves through changing data.&lt;/p&gt;

&lt;p&gt;Follow along for learning more questions and how to actually solve them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
    </item>
    <item>
      <title>System Design Interview: Your Cache Is Fast. Is It Allowed to Be Wrong?</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Tue, 10 Nov 2026 07:08:34 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/system-design-interview-your-cache-is-fast-is-it-allowed-to-be-wrong-41h5</link>
      <guid>https://dev.to/emilywoodsnyc/system-design-interview-your-cache-is-fast-is-it-allowed-to-be-wrong-41h5</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;Before drawing Redis into another architecture, trace one price change through the system. The interesting part happens after the write succeeds.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Imagine finishing a beautiful system design diagram, complete with a cache that makes product pages wonderfully fast.&lt;/p&gt;

&lt;p&gt;Then someone changes the price of a product from $20 to $25. Your database stores the new price, the update succeeds, and a customer still sees $20.&lt;/p&gt;

&lt;p&gt;Okay, perhaps that is acceptable while they browse. But now the same customer clicks Buy, and your checkout trusts the cached price too.&lt;/p&gt;

&lt;p&gt;Suddenly, the conversation has moved from milliseconds to money, and the little Redis box needs a much better explanation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I care less about whether an interview answer includes a cache than whether it explains what happens when that cache is wrong.&lt;/strong&gt; That explanation can be surprisingly concrete, and you do not need to recite every caching strategy to make it useful.&lt;/p&gt;

&lt;p&gt;Let’s work through one small shopping example and build an answer that survives a write, an awkward race, and a cache outage. This is an original practice scenario, not a claim about a particular company’s interview.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ht7lthkc9if4mc1mfx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ht7lthkc9if4mc1mfx7.png" alt="A shopper compares a current database price of $25 with an outdated cached price of $20." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the promise the customer can actually notice
&lt;/h2&gt;

&lt;p&gt;Before choosing an expiration time, separate browsing from purchasing.&lt;/p&gt;

&lt;p&gt;For this example, suppose the product team accepts briefly outdated information on a browsing page, with a clear warning if the final price changes. At checkout, however, the system must validate the applicable price and reserve available stock through the authoritative purchase path.&lt;/p&gt;

&lt;p&gt;That is a proposed product contract. Another product might honor a time-limited quote instead, which would require its own quote identity, expiry, and validation rules.&lt;/p&gt;

&lt;p&gt;Either way, a cache should implement the promise you chose. It should not accidentally choose the promise for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj4cszy1emcsvof0visgd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj4cszy1emcsvof0visgd.png" alt="Browsing and checkout share a product but require different freshness and validation promises." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now name the value you want to cache. A product description, a displayed price, an inventory count, and permission to purchase are different things, even when the UI puts them on the same card.&lt;/p&gt;

&lt;p&gt;You might cache the description for longer than the price. You might show approximate availability while validating the actual reservation transactionally. You should not treat a cached inventory count as permission to sell the final item to two customers.&lt;/p&gt;

&lt;p&gt;This is also where the cache key deserves a sentence. If a displayed price varies by currency, region, or customer agreement, a key containing only the product ID can return the wrong answer even when it is perfectly fresh.&lt;/p&gt;

&lt;p&gt;My scratchpad therefore starts with the cached value, who can share it, how old it may be, and which decisions must use authoritative state. Redis comes after those choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walk through the ordinary path before adding cleverness
&lt;/h2&gt;

&lt;p&gt;Consider a simple cache aside design for public browsing data.&lt;/p&gt;

&lt;p&gt;On a read, the application checks the cache. A hit supplies the browsing response. On a miss, the application reads the database and stores a copy with an expiration time.&lt;/p&gt;

&lt;p&gt;On an update, the application commits the new value to the database and then invalidates the relevant cache entry. The next cache miss loads the value again.&lt;/p&gt;

&lt;p&gt;Microsoft’s &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside" rel="noopener noreferrer"&gt;cache-aside guidance&lt;/a&gt; describes this general pattern and explicitly notes that it does not guarantee consistency between the cache and the data store.&lt;/p&gt;

&lt;p&gt;That caveat is the part worth practicing. A sensible-looking sequence is not the same as a correctness guarantee.&lt;/p&gt;

&lt;p&gt;Even the order of the write and invalidation matters. If you delete the cache entry before committing the database change, a reader can refill the empty cache from the still-old database value.&lt;/p&gt;

&lt;p&gt;Committing first avoids that particular sequence, but there is another one waiting for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let one slow reader interrupt the happy path
&lt;/h2&gt;

&lt;p&gt;Suppose the cache entry is missing and two requests overlap. One request reads the product; the other updates its price.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The reader misses the cache and starts a database read.&lt;/li&gt;
&lt;li&gt;That read obtains the old price of $20, but the reader pauses before filling the cache.&lt;/li&gt;
&lt;li&gt;The writer commits the new price of $25.&lt;/li&gt;
&lt;li&gt;The writer deletes the cache entry and completes its update.&lt;/li&gt;
&lt;li&gt;The reader resumes and stores its earlier $20 result in the cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We deleted after the write, exactly as planned, and still put the old value back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4nlpab0by1si19o2ut5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4nlpab0by1si19o2ut5.png" alt="A five-step interleaving shows an old database read refilling the cache after a writer commits and invalidates it." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nothing in this example requires a replica to lag. The reader already had an old result in flight. A lagging replica can introduce additional stale reads, but this race exists even when that complication is absent.&lt;/p&gt;

&lt;p&gt;An expiration time helps the stale entry disappear eventually. It does not cancel the request that created it. If the old value arrives late and receives a fresh TTL, its age since the underlying update can exceed the TTL you proudly wrote on the diagram.&lt;/p&gt;

&lt;p&gt;So avoid promising that a 30-second TTL means every response is at most 30 seconds behind a committed change. That promise needs more assumptions about read freshness, in-flight work, and when the expiration clock starts.&lt;/p&gt;

&lt;p&gt;This is the moment where I prefer drawing a timeline to adding another technology logo. Five arrows can reveal more than five minutes of vocabulary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose a repair that matches the promise
&lt;/h2&gt;

&lt;p&gt;For our browsing example, accepting some staleness may be the right decision. Use a suitable expiration policy, make failed invalidations observable, and retry invalidation reliably if the design requires it. Describe the remaining race instead of calling the system strongly consistent.&lt;/p&gt;

&lt;p&gt;For checkout, this proposal does something deliberately simpler. It does not let the browsing cache authorize the purchase. The purchase operation validates the applicable price and reserves inventory against authoritative state, with concurrency control that prevents overselling. If that operation cannot safely complete, it should not pretend an old cache value is a successful reservation.&lt;/p&gt;

&lt;p&gt;Notice that reading fresh stock and then decrementing it later would still leave a race. The validation and reservation need to participate in the purchase system’s atomic operation or equivalent concurrency protocol.&lt;/p&gt;

&lt;p&gt;If the interview requires stronger behavior from cached reads themselves, then the design needs more machinery. Version checks can reject older cache fills, but only when the comparison is atomic and the freshness marker survives the races you are trying to prevent. Deleting a value and losing its version does not magically protect the next fill.&lt;/p&gt;

&lt;p&gt;You could also coordinate readers and writers, but now explain lock scope, expiry, failure recovery, and the cost of waiting. Naming a distributed lock is the beginning of that explanation, not the end.&lt;/p&gt;

&lt;p&gt;For this scenario, I prefer a clear split between a fast browsing view and an authoritative purchase decision. It solves the stated product problem without claiming that every read in the application has the same requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work out what happens when the cache disappears
&lt;/h2&gt;

&lt;p&gt;Another tempting answer is that the application will simply fall back to the database.&lt;/p&gt;

&lt;p&gt;Let’s put numbers next to that sentence. Suppose the browsing service receives 10,000 reads per second and its cache hit rate is 99%. In this simplified example, about 100 reads per second reach the database.&lt;/p&gt;

&lt;p&gt;If every request falls through during a cache outage, the database could receive 10,000 reads per second instead. That is up to 100 times the normal read load, before considering retries or other traffic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frx8248ojpm24nv1ksia7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frx8248ojpm24nv1ksia7.png" alt="An illustrative cache outage increases database traffic from 100 reads per second to as many as 10,000." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fallback needs a capacity decision. Bound the number of database requests in flight, set timeouts, and reject or degrade browsing requests when the allowed load is exhausted. If you serve an older copy during a failure, that must fit the browsing contract and requires a copy that remains accessible.&lt;/p&gt;

&lt;p&gt;When a popular key expires, coalescing concurrent misses can let requests share a fetch instead of starting identical database work. Be precise about scope, though. Coalescing inside one process still allows one fetch per process, and the waiting requests need limits too.&lt;/p&gt;

&lt;p&gt;Spreading expiration times can reduce many different keys expiring together. It does not, by itself, stop a crowd from requesting one missing hot key.&lt;/p&gt;

&lt;p&gt;Track hit rate alongside miss latency, database load, invalidation failures, and evidence of stale responses. A high hit rate can look lovely while the cache confidently serves the wrong answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the explanation into a test you can actually run
&lt;/h2&gt;

&lt;p&gt;You do not need a large deployment to practice this reasoning. A small test harness with controllable pauses is enough to expose the refill race.&lt;/p&gt;

&lt;p&gt;Pause a reader immediately after it obtains the old database result. Run the writer through its commit and invalidation. Release the reader, then inspect what the next request returns.&lt;/p&gt;

&lt;p&gt;That gives you a repeatable interleaving, rather than a test that hopes the scheduler produces a bug on the hundredth run.&lt;/p&gt;

&lt;p&gt;For the browsing policy, check whether the resulting stale response is permitted and how it eventually recovers. For checkout, submit a purchase after the price change and verify that the cached browsing value does not decide the accepted price or stock reservation.&lt;/p&gt;

&lt;p&gt;Then make the cache unavailable and send concurrent requests. Check that database concurrency stays bounded and that rejected or degraded requests follow the behavior you promised.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4o17qstrea1jh6qy18g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4o17qstrea1jh6qy18g.png" alt="A reusable worksheet asks for the cached value, sharing boundary, staleness contract, write race, fallback limit, and test." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For your next practice session, choose a read-heavy system from these &lt;a href="https://prachub.com/questions?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;practice questions&lt;/a&gt; and explain just one cached value. Trace its read, its update, and its failure path before expanding the architecture.&lt;/p&gt;

&lt;p&gt;I want to be able to point at the diagram and explain who might see an old value, why that is acceptable, and which operation refuses to trust it.&lt;/p&gt;

&lt;p&gt;Once those answers are clear, the cache box finally earns its place. Until then, we have made the picture faster without deciding whether the customer can trust the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
    </item>
    <item>
      <title>I Can Solve the Question. So Why Do I Freeze on the Follow-Up?</title>
      <dc:creator>Emily Woods</dc:creator>
      <pubDate>Tue, 10 Nov 2026 07:08:33 +0000</pubDate>
      <link>https://dev.to/emilywoodsnyc/i-can-solve-the-question-so-why-do-i-freeze-on-the-follow-up-2kc1</link>
      <guid>https://dev.to/emilywoodsnyc/i-can-solve-the-question-so-why-do-i-freeze-on-the-follow-up-2kc1</guid>
      <description>&lt;h2&gt;
  
  
  One familiar problem, three changed requirements, and a practice routine for the moment your memorized answer runs out.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnntli6motsplfwwkddal.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnntli6motsplfwwkddal.png" alt="A solved notebook path branches when one requirement changes." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting the first solution right can make the next question feel weirdly personal. You were explaining everything so confidently a minute ago, and now someone has changed one requirement and your lovely little hash map suddenly needs a lawyer.&lt;/p&gt;

&lt;p&gt;I don’t think the useful response is to panic and add another fifty problems to the queue. Sometimes the missing practice is much smaller. We learned how to reach an answer, but stopped before learning which assumptions were holding it up.&lt;/p&gt;

&lt;p&gt;Try this before reading further. You have an array of numbers and need two different positions whose values add up to a target. You know the usual hash-map approach. Now the task changes, and you must &lt;strong&gt;count every valid pair of positions instead of returning one pair&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;[1, 1, 3, 3]&lt;/code&gt; with a target of &lt;code&gt;4&lt;/code&gt;, is the answer one, two, or four? More importantly, what information does your original solution throw away?&lt;/p&gt;

&lt;p&gt;That is the small gap we’re going to practise. No new stack, enormous roadmap, or heroic weekend required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first solution has a contract, even if you never wrote it down
&lt;/h2&gt;

&lt;p&gt;For our starting version, assume an unsorted array, exactly one valid pair, and permission to use extra memory. Return the two indices without reusing an element.&lt;/p&gt;

&lt;p&gt;Scan from left to right. Before storing the current value, look for its complement among the values you have already seen. If it is there, you have your pair. With ordinary hash-map assumptions, that takes expected O(n) time and O(n) extra space.&lt;/p&gt;

&lt;p&gt;Notice what makes the solution comfortable. You can stop after the first match. One saved index per value is enough. Keeping previous values in memory is allowed.&lt;/p&gt;

&lt;p&gt;Those details are the handles a follow-up can pull on. I prefer writing them beside the solution because otherwise the code starts to look more universal than it actually is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before changing any code, name what changed, preserve what still works, and find a tiny example that exposes the difference.&lt;/strong&gt; That gives you something useful to do while your brain catches up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fug81r6ivtm7n17on296r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fug81r6ivtm7n17on296r.png" alt="A notebook separates the original contract into output, input and memory assumptions." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Count every pair, and the missing information becomes obvious
&lt;/h2&gt;

&lt;p&gt;Back to &lt;code&gt;[1, 1, 3, 3]&lt;/code&gt;. Label the positions &lt;code&gt;0, 1, 2, 3&lt;/code&gt; and the valid pairs are &lt;code&gt;(0,2)&lt;/code&gt;, &lt;code&gt;(0,3)&lt;/code&gt;, &lt;code&gt;(1,2)&lt;/code&gt; and &lt;code&gt;(1,3)&lt;/code&gt;. We are counting pairs of positions with the earlier index first, so the answer is four.&lt;/p&gt;

&lt;p&gt;If the interviewer means unique pairs of values instead, the answer is one. That clarification belongs before the implementation. You can write perfectly good code for the wrong definition of a pair, which is a very annoying way to spend ten minutes.&lt;/p&gt;

&lt;p&gt;For position pairs, a map that remembers only one index for each value has forgotten something important. When the first &lt;code&gt;3&lt;/code&gt; arrives, it can pair with both earlier &lt;code&gt;1&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;We still need the complement lookup and the left-to-right scan. What changes is the information in the map. Store a frequency for each previous value, and add the complement’s frequency to the answer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Look up first, then increment.&lt;/strong&gt; That order means the map contains only earlier positions, so the current element cannot pair with itself.&lt;/p&gt;

&lt;p&gt;After the two &lt;code&gt;1&lt;/code&gt;s, the map holds a frequency of two. Each subsequent &lt;code&gt;3&lt;/code&gt; contributes two more pairs. For &lt;code&gt;[2, 2, 2]&lt;/code&gt; and target &lt;code&gt;4&lt;/code&gt;, the contributions are zero, one and two, giving three pairs rather than nine.&lt;/p&gt;

&lt;p&gt;Expected running time remains O(n), with O(u) map space for u distinct values. Returning all the pairs would be a different requirement again. The output itself can contain O(n²) pairs, so counting them and listing them cannot share the same blanket performance promise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9a08gl4ptsxn0qjkhxh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9a08gl4ptsxn0qjkhxh.png" alt="Four connections between two ones and two threes show four index pairs, with one value pair distinguished below." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is why I like practising with tiny inputs. Four numbers revealed the ambiguity, the missing state and the update order. A large random test might give you a red result without explaining any of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sorted input changes the opportunity, not just the example
&lt;/h2&gt;

&lt;p&gt;Reset to finding one pair. This time, the input is already sorted and you are asked to use constant extra space. Suppose &lt;code&gt;[1, 2, 4, 7]&lt;/code&gt; has target &lt;code&gt;6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Place one pointer at each end. The first sum is eight, which is too large, so move the right pointer left. Now the sum is five, so move the left pointer right. Two plus four gives the target.&lt;/p&gt;

&lt;p&gt;The explanation matters more than the pointer vocabulary. If the sum is too small, pairing the current left value with any smaller right value cannot help. Sorted order lets us safely discard that left position. The reverse argument applies when the sum is too large.&lt;/p&gt;

&lt;p&gt;We get O(n) time and O(1) auxiliary space because the array is already sorted. If you sort an originally unsorted input yourself, you must account for sorting costs and preserve original indices when the output requires them.&lt;/p&gt;

&lt;p&gt;So the useful response to this follow-up is not immediately reaching for a different memorized template. First identify the new guarantee, then explain which search space it lets you eliminate.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stream changes what you are allowed to forget
&lt;/h2&gt;

&lt;p&gt;Now imagine numbers arriving one at a time, and you need the running count of all pairs seen so far. The frequency-map version still works. Each new value contributes the number of matching earlier values before its own count is added.&lt;/p&gt;

&lt;p&gt;But there is a catch worth saying out loud. Streaming does not automatically mean constant memory. With unrestricted values, this map can keep growing as new distinct values arrive.&lt;/p&gt;

&lt;p&gt;If the follow-up also limits memory, clarify the value range and whether only a recent window matters. A small bounded range can support a fixed-size frequency array. A last-k-items window needs expiration rules and bookkeeping. Those are changed contracts, not free optimizations for the original all-history problem.&lt;/p&gt;

&lt;p&gt;This is a useful place to be calm about uncertainty. You can explain that the current approach remains correct, identify its growing memory cost, and ask which restriction the revised task actually provides.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyj3fm1nxdk0ubxd28spt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyj3fm1nxdk0ubxd28spt.png" alt="Three routes show output changing to counts, sorted input enabling pointers, and a stream retaining growing frequency state." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Give yourself a recovery routine before you need one
&lt;/h2&gt;

&lt;p&gt;When a follow-up feels slippery, take it through four moves. Think of this as a practice scaffold, not a script you have to perform word for word.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Restate the changed requirement.&lt;/strong&gt; For the counting version, clarify whether different positions count separately. Keep unchanged requirements visible too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the smallest revealing input.&lt;/strong&gt; Repeated values expose the difference between one saved index and a frequency. Pick an example whose answer you can justify without running code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the reasoning that survived.&lt;/strong&gt; Complement lookup still helps. Stopping at the first match no longer does. Change the information you retain before rewriting everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the revised cost and failure case.&lt;/strong&gt; Explain time, memory and output size, then test the edge that motivated the change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In a conversation, those moves can flow naturally. We now need every pair of positions, so duplicate values matter. The complement lookup is still useful, but one index per value loses matches. I’ll store frequencies and check before adding the current value so an element never matches itself.&lt;/p&gt;

&lt;p&gt;That explanation gives the interviewer a clear line to follow, even before you finish typing. It also gives you a way back into the problem that does not depend on instantly recognizing its name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make your next practice session slightly less comfortable
&lt;/h2&gt;

&lt;p&gt;Spend twenty minutes on a problem you already understand. Familiarity is useful here because it leaves room to practise the change rather than spending the entire session discovering the baseline.&lt;/p&gt;

&lt;p&gt;For the first five minutes, solve the original version and write down its input guarantees, required output and memory allowance. During the next five, change exactly one requirement and construct a small example that makes the old answer insufficient.&lt;/p&gt;

&lt;p&gt;Use the next seven minutes to adapt the approach, explain what you kept and test the revised behavior. Spend the final three writing one sentence about the assumption you missed. That sentence is more useful than a vague note to revise hashing again.&lt;/p&gt;

&lt;p&gt;You can start with this &lt;a href="https://prachub.com/coding-questions/solve-algorithmic-challenges-in-online-coding-assessments?utm_source=devto&amp;amp;utm_campaign=v1012" rel="noopener noreferrer"&gt;Two Sum practice&lt;/a&gt; , then add the counting variation yourself. The linked problem is the baseline; the changed requirements in this essay are our practice extensions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0novxqq2dgch32ozk02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm0novxqq2dgch32ozk02.png" alt="A twenty-minute practice notebook divides time into baseline, changed rule, adaptation and one lesson." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the next session, choose a different kind of change. Make the input sorted, restrict memory, or ask for a running result. Don’t pile them together immediately, because then it becomes harder to tell which assumption caused the difficulty.&lt;/p&gt;

&lt;p&gt;Judge the session by whether you can explain the change and defend the new solution. Finishing quickly is lovely, but recognizing why your original answer stopped being enough is the skill we came to practise.&lt;/p&gt;

&lt;p&gt;I want my practice notes to contain more than solutions I can reproduce when the question behaves itself. I want them to show what I can keep when it changes, what I need to question, and how I can find my footing again.&lt;/p&gt;

&lt;p&gt;Before closing your next solved problem, give it one follow up and stay for the slightly uncomfortable part. That is where the next practice session begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;~ Emily&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>interview</category>
      <category>leadership</category>
      <category>coding</category>
    </item>
  </channel>
</rss>
