<?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: tejaswipandava</title>
    <description>The latest articles on DEV Community by tejaswipandava (@tejaswipandava).</description>
    <link>https://dev.to/tejaswipandava</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%2F403599%2Fd88ffb6c-e246-4718-b3da-b52602587f30.png</url>
      <title>DEV Community: tejaswipandava</title>
      <link>https://dev.to/tejaswipandava</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tejaswipandava"/>
    <language>en</language>
    <item>
      <title>Designing a URL Shortener (like bit.ly)</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Tue, 20 Oct 2026 07:25:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/designing-a-url-shortener-like-bitly-5h4o</link>
      <guid>https://dev.to/tejaswipandava/designing-a-url-shortener-like-bitly-5h4o</guid>
      <description>&lt;p&gt;Framework: &lt;strong&gt;Problem → Constraint → Decision → Alternative → Trade-off → Failure Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the "hello world" of system design interviews — but it's deceptively deep. The interesting parts aren't the obvious pieces (a redirect service), they're the second-order decisions: how you generate the short code, how you make reads fast at absurd scale, and how you avoid the two classic failure modes (hot keys and analytics write amplification).&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Problem
&lt;/h2&gt;

&lt;p&gt;Build a service that takes a long URL and returns a short one (e.g. &lt;code&gt;https://bit.ly/3xK9pQ&lt;/code&gt;), such that visiting the short URL redirects the user to the original long URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shorten a long URL → return a unique short code.&lt;/li&gt;
&lt;li&gt;Redirect: given a short code, look up and 302/301-redirect to the original URL.&lt;/li&gt;
&lt;li&gt;(Optional) Custom aliases, expiration dates, click analytics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Non-functional requirements / scale assumptions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-heavy: redirects vastly outnumber creations. A realistic ratio is &lt;strong&gt;100:1 to 1000:1&lt;/strong&gt; reads to writes (people click links far more often than they shorten new ones).&lt;/li&gt;
&lt;li&gt;Back-of-envelope: 100M new URLs/day → ~1,150 writes/sec average. At a 100:1 read:write ratio, that's ~115K reads/sec average — spiky well above that for viral links.&lt;/li&gt;
&lt;li&gt;Redirect latency must be very low (~tens of ms) — a URL shortener sitting in the critical path of &lt;em&gt;every&lt;/em&gt; click on a shared link is effectively invisible infrastructure; users notice it only when it's slow.&lt;/li&gt;
&lt;li&gt;High availability for redirects (a link going viral is the worst possible time for the redirect service to be down) — reads should stay up even if the write path degrades.&lt;/li&gt;
&lt;li&gt;Short codes should be &lt;strong&gt;unguessable-ish&lt;/strong&gt; (not sequential enough to enumerate every URL in the system) but don't need cryptographic security for the base case.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Constraint
&lt;/h2&gt;

&lt;p&gt;The constraints that shape every decision below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The short code must be short&lt;/strong&gt; (6-8 chars) but the keyspace must be enormous — 100M URLs/day × several years of retention means billions of entries. Base62 (&lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;) over 7 characters gives &lt;code&gt;62^7 ≈ 3.5 trillion&lt;/code&gt; combinations — comfortably enough, with room for a non-trivial "already taken" collision rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirects must be fast and horizontally scalable&lt;/strong&gt; — this is a lookup-by-key problem at massive read volume, which points immediately toward a key-value store + aggressive caching, not a relational join-heavy schema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No two writers can generate the same code at the same time&lt;/strong&gt; — code generation must be coordination-free (or cheaply coordinated) because writes happen from many stateless app servers concurrently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click analytics (if required) must not slow down or endanger the redirect path&lt;/strong&gt; — the redirect is the SLA-critical operation; analytics is best-effort by comparison.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. Decision
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Short code generation: &lt;strong&gt;Counter + Base62 encoding, sharded&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The core decision: assign every new URL a unique, monotonically-increasing integer ID (from a distributed counter / range-allocated ID block), then encode that integer in Base62 to produce the short code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A central ID-generation service (or a &lt;code&gt;Snowflake&lt;/code&gt;-style ID scheme, or pre-allocated ID ranges handed out to each app server so they can mint IDs locally without a round trip per request) produces the integer.&lt;/li&gt;
&lt;li&gt;Base62-encode the integer → e.g. &lt;code&gt;125&lt;/code&gt; → &lt;code&gt;"cb"&lt;/code&gt;. This is a pure, stateless, deterministic function — no coordination needed at encode time, only at ID-allocation time.&lt;/li&gt;
&lt;li&gt;Store the mapping &lt;code&gt;short_code → long_url&lt;/code&gt; in a key-value store (DynamoDB, Cassandra, or even a sharded relational table keyed by short_code).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this over random-string-and-check?&lt;/strong&gt; Generating a random 7-char string and checking for collision &lt;em&gt;before&lt;/em&gt; insert requires a read-then-write per creation — under high write volume this is wasted round trips and a race window between the check and the insert unless you use a conditional write. A pre-allocated, monotonic counter sidesteps collisions entirely: every ID is unique by construction, no check required.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Storage: &lt;strong&gt;Key-value store, sharded by short_code hash&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Sharding by a hash of &lt;code&gt;short_code&lt;/code&gt; (not by creation time / sequential ID) spreads both reads and writes evenly across shards. This matters because...&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Redirect path: &lt;strong&gt;Cache-first (Redis) in front of the KV store&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Given the 100:1+ read:write ratio, an LRU cache in front of the database captures the vast majority of redirect traffic — especially because real-world link popularity follows a Zipfian/power-law distribution (a small number of links get a huge fraction of all clicks). Cache hit → redirect in single-digit ms without touching the database at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 Analytics: &lt;strong&gt;Asynchronous, off the critical path&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Click events (timestamp, referrer, geo, device) are fired onto a message queue (Kafka/Kinesis) from the redirect handler &lt;em&gt;after&lt;/em&gt; the redirect response has already been sent — never inline/blocking. A separate consumer aggregates these into an analytics store (e.g., a columnar/OLAP store or pre-aggregated counters in Redis) on its own schedule.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Alternative (and why each was rejected as the primary approach)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;th&gt;Why considered&lt;/th&gt;
&lt;th&gt;Why rejected as primary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Random string + collision check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simpler mental model, no central ID allocator needed&lt;/td&gt;
&lt;td&gt;Requires a read-check-write per creation; collision probability rises as the keyspace fills, causing more retries over time (non-constant write latency)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hash the long URL (e.g., MD5/SHA, truncated)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deterministic — same URL always maps to the same short code, which is a nice property (dedup for free)&lt;/td&gt;
&lt;td&gt;Truncated hashes collide far more often than a monotonic counter at the same length (birthday paradox), and you still need a collision-check-and-retry loop; also two &lt;em&gt;different&lt;/em&gt; long URLs can legitimately want &lt;em&gt;different&lt;/em&gt; short codes (can't always dedupe)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fully random UUID, base62-encoded&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No coordination at all, embarrassingly parallel&lt;/td&gt;
&lt;td&gt;UUIDs are 128 bits — encoding one in Base62 still produces a code far longer than 7 chars unless truncated, which reintroduces collisions; defeats the "short" requirement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sequential integer ID, exposed directly (no Base62)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trivial to implement&lt;/td&gt;
&lt;td&gt;Directly reveals total URL count and makes the entire link space enumerable/guessable — every 4th short URL exists and is trivially walkable by an attacker/scraper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Synchronous analytics write on every redirect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple, strongly consistent click counts&lt;/td&gt;
&lt;td&gt;Couples the SLA-critical redirect path to the availability/latency of the analytics store; a slow analytics DB now slows down every single click in the product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Single relational DB, no cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simplest possible architecture, easy joins for custom features&lt;/td&gt;
&lt;td&gt;Cannot survive a viral link — a single link suddenly taking 50K reads/sec will saturate a single DB's connection pool and starve every &lt;em&gt;other&lt;/em&gt; link's redirects too (a self-inflicted Hot Partition, see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  5. Trade-off
&lt;/h2&gt;

&lt;p&gt;Every decision above is a trade-off, not a free win:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Counter + Base62 vs. random string&lt;/strong&gt;: gains collision-free generation and predictable write latency, but &lt;em&gt;loses&lt;/em&gt; the "any node can mint an ID with zero coordination" property — you now depend on an ID-allocation service (or pre-fetched ID ranges) being available. This is mitigated by handing out ID &lt;em&gt;ranges&lt;/em&gt; (e.g., blocks of 10,000 IDs) to each app server in advance, so 99.99% of ID mints are purely local and the allocator is only hit occasionally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache-first redirects&lt;/strong&gt;: gains massive read throughput and low latency, but &lt;em&gt;loses&lt;/em&gt; strong consistency — if a URL's target is updated (rare, but supported by some shorteners) or a link is deleted/deactivated, cached readers may serve the stale mapping until TTL expiry or explicit cache invalidation. Acceptable trade-off because URL→target mappings are near-immutable in practice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async analytics&lt;/strong&gt;: gains a redirect path that never blocks on analytics infra, but &lt;em&gt;loses&lt;/em&gt; real-time click counts — the count shown on a dashboard lags real clicks by however long the queue+consumer take to catch up (typically seconds, not milliseconds). Acceptable because nobody needs a &lt;em&gt;live&lt;/em&gt; click counter with sub-second accuracy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharding by short_code hash&lt;/strong&gt;: gains even load distribution, but &lt;em&gt;loses&lt;/em&gt; the ability to cheaply range-scan "all URLs created in the last hour" (a query a naive sequential-ID layout would support trivially) — if that query is needed, it has to be served from a separate index/analytics store, not the primary KV store.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. Failure Mode
&lt;/h2&gt;

&lt;p&gt;This is where the URL shortener problem gets interesting — the "trivial" system has real failure modes that mirror classic distributed-systems patterns:&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 Hot Partition / Hot Key — the viral link problem
&lt;/h3&gt;

&lt;p&gt;A single short URL going viral (posted on the front page of a major site, shared in a mass text) concentrates &lt;em&gt;all&lt;/em&gt; of its read traffic onto one shard/partition and, if uncached, one DB row. This is the textbook &lt;strong&gt;Hot Partition&lt;/strong&gt; failure mode: one key gets so much traffic that its home shard degrades for every &lt;em&gt;other&lt;/em&gt; key sharing that shard too — a single viral link can take down redirects for unrelated URLs that happen to co-locate on the same partition.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mitigation&lt;/strong&gt;: cache-first design (Section 3.3) absorbs the vast majority of hot-key traffic before it ever reaches the sharded store. For truly extreme spikes, a second layer — a CDN-edge cache or even a static-redirect rule pushed to the CDN — takes it further upstream than application-layer Redis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.2 ID-allocator becoming a single point of failure
&lt;/h3&gt;

&lt;p&gt;If ID generation is centralized (one counter service) and it goes down, &lt;strong&gt;all writes&lt;/strong&gt; (new shortens) fail even though reads (redirects) are completely unaffected by this outage. This is a good interview point: correctly identify that this failure is &lt;em&gt;write-path-only&lt;/em&gt; and does not threaten the SLA-critical redirect path, then propose the pre-allocated ID-range mitigation from Section 5 so that even a brief allocator outage doesn't stall writes (app servers keep minting from their local range until it's exhausted).&lt;/p&gt;

&lt;h3&gt;
  
  
  6.3 Cache stampede on a cold, suddenly-popular key
&lt;/h3&gt;

&lt;p&gt;A link goes viral at the exact moment its cache entry expires (or it was never cached because it's brand new but shared immediately) — thousands of concurrent redirect requests all miss the cache simultaneously and all hammer the database for the same key at once. This is the same &lt;strong&gt;cache stampede&lt;/strong&gt; pattern documented in &lt;code&gt;article: Caching Strategies: 8 Patterns Every Engineer Should Know&lt;/code&gt;, and the same fix applies: request coalescing (in-flight de-duplication) so only one request per key actually reaches the database while the rest wait on that result.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.4 Analytics write amplification
&lt;/h3&gt;

&lt;p&gt;If click-event volume isn't decoupled from the redirect path (Decision 3.4), a viral link doesn't just amplify &lt;em&gt;read&lt;/em&gt; load — it amplifies &lt;em&gt;write&lt;/em&gt; load on whatever store holds analytics, at the same multiplier. Without the async-queue buffer, this write spike can back up and, in the worst case, apply backpressure all the way to the redirect handler itself, turning an analytics slowdown into a user-facing outage — the exact &lt;strong&gt;Cascading Failure&lt;/strong&gt; pattern from &lt;code&gt;10 Must-Know System Design Failure Modes&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.5 Short-code exhaustion / collision creep (long-tail failure)
&lt;/h3&gt;

&lt;p&gt;Not an acute outage, but a slow-burn failure mode: if the ID-range or code length was under-provisioned relative to actual growth, the system approaches keyspace exhaustion years into production, forcing a length migration (7→8 chars) that must be backward-compatible with every already-issued short code still in circulation. Worth calling out in an interview as the kind of failure mode that doesn't page anyone at 3am but &lt;em&gt;will&lt;/em&gt; show up in a capacity-planning review — the fix is over-provisioning the code length up front (each added Base62 character multiplies the keyspace by 62×, so the cost of provisioning generously is nearly free).&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The "trivial" URL shortener interview question is really a &lt;strong&gt;read-scaling + hot-key&lt;/strong&gt; problem wearing a simple disguise — the moment you introduce "what if a link goes viral," every classic caching/sharding failure mode becomes relevant.&lt;/li&gt;
&lt;li&gt;Counter + Base62 beats random-string-and-check specifically because it makes collisions &lt;em&gt;structurally impossible&lt;/em&gt; rather than &lt;em&gt;statistically rare&lt;/em&gt; — a good general principle: prefer designs that eliminate a failure mode by construction over designs that merely reduce its probability.&lt;/li&gt;
&lt;li&gt;Decoupling the SLA-critical path (redirect) from the best-effort path (analytics) via an async queue is the same pattern that shows up in payment webhooks, notification delivery, and demand-forecasting pipelines elsewhere in this vault — it's a general recipe, not a URL-shortener-specific trick.&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>techlettersbyteja</category>
      <category>systemdesign</category>
      <category>casestudy</category>
    </item>
    <item>
      <title>Stop Rambling in System Design Interviews — 3 Frameworks That Fix It</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Mon, 19 Oct 2026 03:06:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/stop-rambling-in-system-design-interviews-3-frameworks-that-fix-it-95o</link>
      <guid>https://dev.to/tejaswipandava/stop-rambling-in-system-design-interviews-3-frameworks-that-fix-it-95o</guid>
      <description>&lt;h2&gt;
  
  
  The RADIO Framework: How to Structure Any System Design Interview Answer
&lt;/h2&gt;

&lt;p&gt;Writing good code alone doesn't clear Senior SDE / IC-level system design rounds anymore. What separates strong candidates from everyone else isn't raw knowledge of architectures — it's the ability to &lt;strong&gt;think out loud in a structured way under pressure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A few hard truths about system design rounds: you can't walk in and improvise, hoping structure will emerge on its own. You can't rely on your resume or past projects to "speak for itself" in the room — the interviewer only sees what you say in the next 45 minutes. And you can't skip structure and expect the interviewer to connect the dots for you; it's your job to make the narrative obvious.&lt;/p&gt;

&lt;p&gt;Below are three complementary frameworks I use to stay organized in these interviews: &lt;strong&gt;RADIO&lt;/strong&gt; as the top-level skeleton, a &lt;strong&gt;layered checklist&lt;/strong&gt; for the gritty details inside each RADIO step, and a &lt;strong&gt;4-step scoping approach&lt;/strong&gt; for nailing the critical opening minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  RADIO: A 5-Step Framework for System Design Answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;RADIO&lt;/strong&gt; is a simple mental checklist to organize any system design answer, so you stay clear and methodical instead of rambling, without ever sounding robotic or scripted.&lt;/p&gt;

&lt;h3&gt;
  
  
  R — Requirements
&lt;/h3&gt;

&lt;p&gt;Start by scoping the problem before designing anything. What must the system do — the functional requirements? What must it &lt;em&gt;not&lt;/em&gt; break — the non-functional requirements like availability, latency, and consistency? And what are the rough scale estimates — users, QPS, data volume — that set the boundaries for everything that follows?&lt;/p&gt;

&lt;h3&gt;
  
  
  A — Architecture
&lt;/h3&gt;

&lt;p&gt;Zoom out to the big picture before diving into details. Identify the major components and how they connect. Decide the client/server communication style — REST, gRPC, WebSockets, message queues. Most importantly, call out key architectural decisions and &lt;em&gt;why&lt;/em&gt; you made them — this is where you demonstrate judgment, not just knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  D — Data Model
&lt;/h3&gt;

&lt;p&gt;Think about how data is shaped, stored, and accessed. What entities or tables exist, and how are they related? What needs to be indexed for fast lookups? What needs to be cached, and what remains the source of truth?&lt;/p&gt;

&lt;h3&gt;
  
  
  I — Interfaces (APIs)
&lt;/h3&gt;

&lt;p&gt;Define the contracts between components. Keep the API surface minimal and purposeful — every endpoint should map back to a real requirement from step one. Specify request/response shapes only for the core operations, and skip anything that isn't essential to the problem at hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  O — Optimizations
&lt;/h3&gt;

&lt;p&gt;Don't save this for the last two minutes of the interview — weave it in as you go. Proactively call out what could go wrong: hotspots, single points of failure, slow queries. Discuss how you'd handle scale, load spikes, and latency using techniques like caching, sharding, async processing, and rate limiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;RADIO isn't a trick or a shortcut — it's a way to stay organized and confident under pressure. It gives you a repeatable structure so you always know "what to say next," which frees up mental bandwidth to actually reason about the problem instead of worrying about how to present it. Once internalized, it shifts you from reactively answering questions to &lt;strong&gt;proactively leading the conversation&lt;/strong&gt; — which is exactly the signal interviewers look for at the Senior SDE / IC4+ level.&lt;/p&gt;




&lt;h2&gt;
  
  
  Framework #2: The Layered System Design Checklist
&lt;/h2&gt;

&lt;p&gt;Most engineers don't fail system design interviews because they lack knowledge — &lt;strong&gt;they fail because they panic midway and lose structure.&lt;/strong&gt; A strong framework beats random brilliance. Think of this as a complementary, more granular checklist to RADIO above: it's what to actually say &lt;em&gt;inside&lt;/em&gt; the "Architecture" and "Optimizations" steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Start With the Client Journey
&lt;/h3&gt;

&lt;p&gt;Who are the users? What requests are coming in, and at what scale? Trace a single request end-to-end before designing any components — this grounds every later decision in a real flow instead of a set of abstract boxes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Define the Entry Layer
&lt;/h3&gt;

&lt;p&gt;Load balancers, API gateways, authentication, rate limiting, caching. Most candidates jump directly to databases — &lt;strong&gt;strong candidates first control traffic&lt;/strong&gt;, deciding how requests are authenticated, throttled, and routed before deciding how data is stored.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Separate Control Plane vs. Data Plane
&lt;/h3&gt;

&lt;p&gt;Metadata and coordination services (the control plane) should not behave the same way as bulk storage and transfer services (the data plane) — they have different consistency, latency, and scaling needs. Calling out this distinction explicitly is a strong signal of architectural maturity.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Think Asynchronously
&lt;/h3&gt;

&lt;p&gt;Use queues everywhere it makes sense: notifications, feed generation, video processing, search indexing. If everything is synchronous, the system collapses under scale — you need to decouple producer load from consumer processing rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Design for Scale From Day One
&lt;/h3&gt;

&lt;p&gt;Partitioning, replication, caching, CDNs, sharding. Interviewers care about &lt;strong&gt;why&lt;/strong&gt; you chose a technique, not just that you can name it — always follow up each term with the specific bottleneck it solves.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Observability Matters
&lt;/h3&gt;

&lt;p&gt;Distributed tracing, logging, monitoring, metrics — most candidates forget this entirely. Production systems fail &lt;em&gt;silently&lt;/em&gt; without visibility, so call out what you'd monitor and alert on, not just what you'd build.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Finally, Discuss Trade-offs
&lt;/h3&gt;

&lt;p&gt;Consistency vs. availability, latency vs. cost, SQL vs. NoSQL, push vs. pull architecture. &lt;strong&gt;This is where the real interview begins.&lt;/strong&gt; The best architects aren't the ones who know the most tools — they're the ones who think in layers, constraints, bottlenecks, and trade-offs.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Two Frameworks Fit Together
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RADIO Step&lt;/th&gt;
&lt;th&gt;Layered Checklist Items&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requirements&lt;/td&gt;
&lt;td&gt;Client journey (users, requests, scale)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Entry layer, control vs. data plane, async/queues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Model&lt;/td&gt;
&lt;td&gt;Partitioning, replication, sharding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interfaces&lt;/td&gt;
&lt;td&gt;(API contracts — same as RADIO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimizations&lt;/td&gt;
&lt;td&gt;Caching/CDNs, observability, trade-off discussion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use &lt;strong&gt;RADIO&lt;/strong&gt; as the high-level narrative order you speak in, and this &lt;strong&gt;layered checklist&lt;/strong&gt; as the detailed prompts to make sure you don't forget a dimension — entry layer, control/data plane split, async, observability, trade-offs — while inside each RADIO step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Framework #3: The 4-Step Scoping-First Approach
&lt;/h2&gt;

&lt;p&gt;Most engineers fail system design interviews not because they lack knowledge, but because they lack structure. You don't need to memorize a hundred architectures — you need a structured, repeatable approach. This framework focuses specifically on &lt;strong&gt;how to open&lt;/strong&gt; a system design interview correctly. It maps most closely onto RADIO's &lt;strong&gt;R (Requirements)&lt;/strong&gt; and the start of &lt;strong&gt;A (Architecture)&lt;/strong&gt;, with much more concrete detail on &lt;em&gt;how&lt;/em&gt; to scope and estimate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Clarify the Problem Before Writing Anything
&lt;/h3&gt;

&lt;p&gt;System design interviews aren't about throwing out buzzwords — they're about trade-offs. Most candidates assume things and jump straight into solutions, so make sure you actually know what you're solving first. Nail down the &lt;strong&gt;scoping&lt;/strong&gt;: are we designing just one feature, or the entire system — "just the video upload pipeline" versus "all of YouTube"? Clarify the &lt;strong&gt;constraints upfront&lt;/strong&gt;: how many users, what's the read/write ratio, what are the latency requirements? And agree on the &lt;strong&gt;success criteria&lt;/strong&gt;: what matters most here — scalability, cost, or low latency? These often trade off against each other, so naming the priority early shapes every later decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Define Functional and Non-Functional Requirements Clearly
&lt;/h3&gt;

&lt;p&gt;Nail down the &lt;strong&gt;functional&lt;/strong&gt; requirements — what features does the system actually need to support? Then the &lt;strong&gt;non-functional&lt;/strong&gt; requirements — what are the performance expectations around availability, latency, consistency, and durability? Most importantly, identify the biggest technical challenge up front, since this single question should guide the rest of the discussion.&lt;/p&gt;

&lt;p&gt;For example, if you're designing YouTube, is the focus on video uploads, recommendations, or live streaming? Each of those has a completely different set of constraints — uploads mean large binary transfer plus transcoding, recommendations mean an ML/data pipeline, live streaming means ultra-low-latency real-time delivery — so the interviewer's emphasis should steer where you spend your time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Estimate the Scale and Plan Capacity Like an Engineer
&lt;/h3&gt;

&lt;p&gt;Most candidates throw out random numbers. Instead, do quick back-of-the-envelope calculations to validate assumptions. Derive &lt;strong&gt;users and requests per second&lt;/strong&gt; from the stated daily/monthly active users and typical usage patterns, not guesses. Work out &lt;strong&gt;storage needs&lt;/strong&gt; with concrete math — for example, "10MB per user × 100M users = 1PB" — not hand-waving. And figure out &lt;strong&gt;throughput&lt;/strong&gt;: can a single database handle this load, or does it clearly need sharding or read replicas from the outset? The estimate should directly justify later design decisions, e.g., "at this write volume, a single primary DB would bottleneck, so we shard by user ID."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Break the System Into Core Components (High-Level Design)
&lt;/h3&gt;

&lt;p&gt;Define the major building blocks: API gateway, load balancer, service layers, databases. Don't overcomplicate things — simple and scalable always wins over an impressively complex diagram that doesn't map to the actual requirements gathered in steps one through three. Clearly define the interactions and data flow between services.&lt;/p&gt;

&lt;p&gt;For a messaging app, this might break down into a &lt;strong&gt;User Service&lt;/strong&gt; for auth and profiles, a &lt;strong&gt;Messaging Service&lt;/strong&gt; for storing and retrieving chats, a &lt;strong&gt;Notification Service&lt;/strong&gt; for real-time updates, and &lt;strong&gt;Media Storage&lt;/strong&gt; for images and videos. Each component has different constraints — messaging needs low-latency writes, media storage needs cheap bulk blob storage, notifications need a push/fan-out mechanism — so build around what's most important for &lt;em&gt;this&lt;/em&gt; system's stated priorities, not a generic template.&lt;/p&gt;

&lt;h3&gt;
  
  
  How This Maps Onto RADIO
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RADIO Step&lt;/th&gt;
&lt;th&gt;This Framework's Steps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requirements&lt;/td&gt;
&lt;td&gt;Step 1 (Scoping) + Step 2 (Functional/Non-functional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture&lt;/td&gt;
&lt;td&gt;Step 3 (Capacity estimation justifies architecture) + Step 4 (Core components)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Model / Interfaces / Optimizations&lt;/td&gt;
&lt;td&gt;Covered by RADIO + the Layered Checklist as the design deepens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; use this framework specifically to nail the opening ten to fifteen minutes of any system design interview — scope tightly, quantify the scale with real numbers, then break into components — before moving into the deeper RADIO and layered-checklist steps for data modeling, APIs, and optimizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;None of these three frameworks replace actual system design knowledge — you still need to understand caching, sharding, queues, and consistency models. What they give you is a &lt;strong&gt;scaffold to hang that knowledge on&lt;/strong&gt;, so under interview pressure you're navigating a familiar checklist instead of improvising from scratch. Start every interview with the scoping-first approach, narrate your answer using RADIO, and lean on the layered checklist whenever you need to go one level deeper on architecture or optimizations.&lt;/p&gt;

&lt;p&gt;If you've got your own mental framework for system design interviews, I'd love to hear it in the comments.&lt;/p&gt;

</description>
      <category>techlettersbyteja</category>
      <category>systemdesign</category>
      <category>careerdevelopment</category>
    </item>
    <item>
      <title>Designing APIs for Massive Datasets (Cursor Pagination, Sparse Fields, Caching)</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:28:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/designing-apis-for-massive-datasets-cursor-pagination-sparse-fields-caching-1dh3</link>
      <guid>https://dev.to/tejaswipandava/designing-apis-for-massive-datasets-cursor-pagination-sparse-fields-caching-1dh3</guid>
      <description>&lt;p&gt;&lt;strong&gt;The scenario:&lt;/strong&gt; &lt;em&gt;Your API returns a list of 2 million products. A mobile app needs to display them. How do you design this so the app doesn't crash and the server doesn't melt?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The naive answer — return everything in one response — fails immediately at scale: 2 million records at even a modest ~400 bytes each is ~800MB of JSON, which will crash a memory-constrained mobile client and saturate a slow connection long before it renders anything. The real answer is a stack of five complementary techniques, each solving a different bottleneck.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Cursor-Based Pagination (not offset-based)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem with offset pagination
&lt;/h3&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&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;id&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;999980&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what &lt;code&gt;?page=50000&amp;amp;size=20&lt;/code&gt; compiles down to. The database still has to &lt;strong&gt;scan and discard&lt;/strong&gt; the first 999,980 matching rows before it can return the 20 you actually asked for — even though an index exists on &lt;code&gt;id&lt;/code&gt;/&lt;code&gt;ORDER BY&lt;/code&gt; column, &lt;code&gt;OFFSET&lt;/code&gt; forces the DB to walk that many index entries and skip them. The deeper the page, the slower the query — page 1 is instant, page 50,000 can take seconds, and the cost grows roughly linearly with offset depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: cursor (a.k.a. keyset) pagination
&lt;/h3&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_seen_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;id&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of asking for "page 50,000," the client says &lt;strong&gt;"give me 20 records after this specific ID (or timestamp, or composite key)."&lt;/strong&gt; The database uses the index to seek directly to &lt;code&gt;:last_seen_id&lt;/code&gt; and reads forward — &lt;strong&gt;O(log n) seek + O(page size) read&lt;/strong&gt;, regardless of how deep into the dataset you are. Page 1 and page 100,000 cost the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API shape:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/products?limit=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;cursor=eyJpZCI&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;MTAwMjB&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;base&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="err"&gt;-encoded&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;cursor,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;e.g.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10020&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;Response:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"next_cursor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJpZCI6MTAwNDB9"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Trade-offs worth stating explicitly:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Cursor pagination &lt;strong&gt;can't jump to an arbitrary page number&lt;/strong&gt; ("go to page 500") — only forward/backward from a known cursor. If a UI truly needs numbered page jumps (rare for infinite scroll, common for admin tables), offset pagination (or a hybrid) may still be needed for that specific view.&lt;/li&gt;
&lt;li&gt;✅ Cursors should encode a &lt;strong&gt;unique, stable sort key&lt;/strong&gt; (or tuple, e.g., &lt;code&gt;(created_at, id)&lt;/code&gt; to break ties) — using a non-unique column alone can cause skipped or duplicated rows if two rows share the same value.&lt;/li&gt;
&lt;li&gt;✅ Works cleanly with infinite-scroll UIs (the dominant mobile pattern), which never need to jump to an arbitrary page anyway.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Sparse Field Selection (Response Shaping)
&lt;/h2&gt;

&lt;p&gt;A product with 30 fields (full description, specs, reviews, inventory count, etc.) might be ~4KB. The list screen only renders &lt;strong&gt;name, price, and thumbnail URL&lt;/strong&gt; — maybe 200 bytes. Returning full objects for a list view is pure waste: &lt;strong&gt;20x more data transferred than what's rendered.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Return a &lt;strong&gt;lean DTO/projection&lt;/strong&gt; for list endpoints — not the same serialization used for the detail endpoint.&lt;/li&gt;
&lt;li&gt;Detail fields (description, specs, reviews) load &lt;strong&gt;on-demand&lt;/strong&gt; when the user taps into a specific item (&lt;code&gt;GET /products/{id}&lt;/code&gt; — a separate, richer response).&lt;/li&gt;
&lt;li&gt;Optionally support a &lt;code&gt;fields=&lt;/code&gt; query param (GraphQL-style field selection, or a simple comma-separated whitelist) so different client screens can request exactly what they need without needing a new endpoint per view.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; &lt;em&gt;design list/detail as two different response shapes from day one&lt;/em&gt; — don't let the detail DTO become the "default" that list endpoints lazily reuse.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Database-Level Filtering (push filters down, never up)
&lt;/h2&gt;

&lt;p&gt;If a user searches "bluetooth speaker" against 2 million products:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;Worst:&lt;/strong&gt; fetch all 2M rows, filter in the mobile app.&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Bad:&lt;/strong&gt; fetch all 2M rows, filter in the application/server layer.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Correct:&lt;/strong&gt; push the filter into the &lt;strong&gt;database query&lt;/strong&gt; (&lt;code&gt;WHERE name ILIKE '%bluetooth%'&lt;/code&gt; backed by a proper index, or a dedicated search engine like Elasticsearch for full-text search at this scale) so the database does the heavy lifting using indexes, and the server only ever receives and forwards the ~340 matching rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this matters beyond just this example:&lt;/strong&gt; any filtering logic that runs &lt;em&gt;after&lt;/em&gt; data leaves the database has already paid the full cost of reading/transferring the unfiltered dataset — the filtering step itself becomes irrelevant to performance at that point. The entire goal is to &lt;strong&gt;narrow the result set as early as possible in the pipeline.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Aggressive Caching of Early Pages
&lt;/h2&gt;

&lt;p&gt;Real usage data almost always follows a power law: &lt;strong&gt;~80% of users never scroll past page 3.&lt;/strong&gt; This makes the first few pages of the most common queries (default sort, popular filters/search terms) extremely cache-friendly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache the &lt;strong&gt;first N pages&lt;/strong&gt; (e.g., first 5) of common queries in Redis, keyed by &lt;code&gt;(query_params, cursor)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Most user sessions are served entirely from cache and never touch the database.&lt;/li&gt;
&lt;li&gt;The database only handles &lt;strong&gt;cache misses and deep-pagination requests&lt;/strong&gt; — a small, predictable fraction of total traffic.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;(See &lt;code&gt;Caching Strategies.md&lt;/code&gt; for cache-aside/TTL/invalidation patterns to apply here — this is a textbook cache-aside candidate with a short TTL, since product catalogs change but not on every request.)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Response Compression
&lt;/h2&gt;

&lt;p&gt;A trivial but high-leverage win: enable &lt;strong&gt;gzip (or brotli) compression&lt;/strong&gt; on API responses.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A ~50KB JSON payload commonly compresses down to ~8KB over the wire — JSON's repetitive key names and text-heavy structure compress extremely well.&lt;/li&gt;
&lt;li&gt;On slow/high-latency mobile networks (3G, poor signal), this is the difference between a multi-second load and a near-instant one — compression reduces &lt;strong&gt;transfer time&lt;/strong&gt;, which dominates total latency on constrained networks far more than server-side processing time does.&lt;/li&gt;
&lt;li&gt;Usually a &lt;strong&gt;single configuration line&lt;/strong&gt; (e.g., &lt;code&gt;gzip on;&lt;/code&gt; in Nginx, or a compression middleware in the app framework) — no application code changes required.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Full Architecture, Stacked
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client (mobile app)
   │  GET /products?limit=20&amp;amp;cursor=... &amp;amp;q=bluetooth+speaker
   ▼
API Gateway / Load Balancer  ── gzip compression on response ──►
   ▼
App Server
   │  1. Parses cursor + filter params
   │  2. Checks Redis cache for (query, cursor) — HIT for ~80% of requests (early pages)
   ▼
   on cache MISS:
   │  3. Pushes filter + keyset condition to DB:
   │       SELECT id, name, price, thumbnail_url        ← sparse fields
   │       FROM products
   │       WHERE name @@ 'bluetooth speaker'             ← DB-level filter (indexed / search engine)
   │         AND id &amp;gt; :cursor
   │       ORDER BY id LIMIT 20                          ← cursor pagination, index seek
   │  4. Writes result back into Redis (short TTL)
   ▼
Response: { items: [ {id, name, price, thumbnail_url}, ... ], next_cursor: "..." }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer removes an order of magnitude of unnecessary work from the layer above it — by the time data reaches the client, it's a tiny, precisely-shaped, compressed payload instead of the naive 800MB firehose.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Solves&lt;/th&gt;
&lt;th&gt;Cost if skipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cursor pagination&lt;/td&gt;
&lt;td&gt;Deep-page query slowness&lt;/td&gt;
&lt;td&gt;O(offset) scan cost that grows with dataset size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sparse fields&lt;/td&gt;
&lt;td&gt;Oversized payloads per item&lt;/td&gt;
&lt;td&gt;~20x more bytes transferred than needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB-level filtering&lt;/td&gt;
&lt;td&gt;Wasted compute/transfer on irrelevant rows&lt;/td&gt;
&lt;td&gt;Full table scan/transfer before any filtering happens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching early pages&lt;/td&gt;
&lt;td&gt;Redundant DB load for the most common requests&lt;/td&gt;
&lt;td&gt;Every request (even identical ones) hits the DB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression&lt;/td&gt;
&lt;td&gt;Wire transfer time on slow networks&lt;/td&gt;
&lt;td&gt;5-6x larger payload size over the wire&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Meta-Lesson
&lt;/h2&gt;

&lt;p&gt;None of these five techniques are individually complex — each is a well-known, "simple" optimization. The real insight isn't knowing any single technique; it's &lt;strong&gt;recognizing that they compose, and each targets a different bottleneck in the pipeline&lt;/strong&gt; (query cost, payload size, filtering cost, repeated-request cost, transfer cost). A shallow answer stops at "add pagination." A deeper answer explains &lt;em&gt;why offset pagination specifically breaks down at scale&lt;/em&gt;, and layers the remaining four techniques on top with a clear rationale for each.&lt;/p&gt;

</description>
      <category>api</category>
      <category>performance</category>
      <category>scalability</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>The 1-2-3 Method: Preparing for SDE-2/3 Roles Alongside a 9-5</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Sun, 18 Oct 2026 04:18:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/the-1-2-3-method-preparing-for-sde-23-roles-alongside-a-9-5-1idg</link>
      <guid>https://dev.to/tejaswipandava/the-1-2-3-method-preparing-for-sde-23-roles-alongside-a-9-5-1idg</guid>
      <description>&lt;h2&gt;
  
  
  Core Insight
&lt;/h2&gt;

&lt;p&gt;You don't need extra time — you need a &lt;strong&gt;better system&lt;/strong&gt;. Long hours, deadlines, and meetings don't have to block prep if the structure is right.&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣ One Pattern at a Time
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Instead of scattered, random practice, focus on &lt;strong&gt;1 DSA pattern per week&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Pair it with &lt;strong&gt;1 System Design case study every weekend&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Depth &amp;gt; breadth: mastering a pattern (e.g., sliding window, two pointers, topological sort) generalizes to dozens of problems, instead of grinding random questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Weekly cadence example:&lt;/strong&gt;&lt;br&gt;
| Day | Focus |&lt;br&gt;
|-----|-------|&lt;br&gt;
| Mon-Fri | 1 DSA pattern, ~2 hrs/day (theory + 3-5 problems) |&lt;br&gt;
| Sat | System design case study (read/design + notes) |&lt;br&gt;
| Sun | Mock interview + review weak spots |&lt;/p&gt;




&lt;h2&gt;
  
  
  2️⃣ Two Hours of Consistency
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2 focused hours daily&lt;/strong&gt; beat 6 distracted hours crammed into a Sunday.&lt;/li&gt;
&lt;li&gt;Small, steady progress compounds — same principle as spaced repetition and incremental system building.&lt;/li&gt;
&lt;li&gt;Protect this block like a meeting: no phone, single task, timer-boxed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tips to protect the 2 hours:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Block it on your calendar like a work meeting.&lt;/li&gt;
&lt;li&gt;Do it early morning or right after work before energy drops.&lt;/li&gt;
&lt;li&gt;Track streaks (not just problems solved) to stay motivated.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3️⃣ Three Mock Interviews for Better Communication
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sit in &lt;strong&gt;mock interviews every weekend&lt;/strong&gt; — coding + system design + behavioral.&lt;/li&gt;
&lt;li&gt;Advanced SDE interviews (SDE-2/3) test &lt;strong&gt;trade-offs and clarity of thought&lt;/strong&gt;, not just correctness.&lt;/li&gt;
&lt;li&gt;Behavioral rounds matter as much as coding speed — practice articulating decisions out loud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What to rotate across mocks:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DSA mock — focus on communicating approach before coding.&lt;/li&gt;
&lt;li&gt;System design mock — focus on trade-off discussions (consistency vs availability, SQL vs NoSQL, etc.).&lt;/li&gt;
&lt;li&gt;Behavioral mock — STAR-format stories, leadership/conflict scenarios.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Removes the "no time" excuse by fitting prep into a realistic daily/weekly rhythm instead of requiring large uninterrupted blocks.&lt;/li&gt;
&lt;li&gt;Builds compounding skill (patterns) + compounding habit (consistency) + compounding confidence (mock reps).&lt;/li&gt;
&lt;li&gt;Balances technical depth (DSA/System Design) with communication (mocks), which is what actually gets evaluated at SDE-2/3 bar-raiser rounds.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Personal Action Plan (fill in)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pick this week's DSA pattern: ___________&lt;/li&gt;
&lt;li&gt;[ ] Pick this weekend's system design case: ___________&lt;/li&gt;
&lt;li&gt;[ ] Schedule daily 2-hour block: ___________ (time of day)&lt;/li&gt;
&lt;li&gt;[ ] Schedule 3 mocks for the weekend: DSA / System Design / Behavioral&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>techlettersbyteja</category>
      <category>careerdevelopment</category>
    </item>
    <item>
      <title>Caching Strategies: 8 Patterns Every Engineer Should Know</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Sat, 17 Oct 2026 04:27:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/caching-strategies-8-patterns-every-engineer-should-know-pa9</link>
      <guid>https://dev.to/tejaswipandava/caching-strategies-8-patterns-every-engineer-should-know-pa9</guid>
      <description>&lt;p&gt;&lt;strong&gt;The fastest way to produce a shallow system design:&lt;/strong&gt; saying &lt;em&gt;"I'll add Redis for caching"&lt;/em&gt; and stopping there. Naming "Redis" without explaining &lt;strong&gt;what&lt;/strong&gt; you're caching and &lt;strong&gt;why&lt;/strong&gt; signals you're naming a tool, not solving a problem.&lt;/p&gt;

&lt;p&gt;The real question isn't "would you use Redis?" — it's &lt;strong&gt;"which caching pattern fits this access pattern?"&lt;/strong&gt; Choosing the wrong strategy leads to stale data, cache stampedes, consistency issues, and poor performance, regardless of which underlying technology you picked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Right Way to Think About Caching
&lt;/h2&gt;

&lt;p&gt;Never say "for caching." Say something with an actual access pattern attached:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"User session data, accessed 10K/sec, TTL 30 minutes, cache-aside pattern."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Hot product listings — 20% of items get 80% of reads, LRU eviction."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Computed aggregates, refreshed every 5 minutes, write-behind to reduce DB load."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these names: &lt;strong&gt;what's being cached, how hot it is, how long it should live, and which read/write pattern fits it.&lt;/strong&gt; That's the difference between reciting a buzzword and demonstrating real design judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule: access pattern first, technology second — always.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The 8 Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1️⃣ Cache-Aside (Lazy Loading)
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;application&lt;/strong&gt; (not the cache) is responsible for the loading logic — the app makes &lt;strong&gt;two separate calls&lt;/strong&gt;, one to the cache and (on a miss) a second one to the DB, and the app itself writes the result back into the cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App checks cache first
    ├─ HIT  → return directly
    └─ MISS → App queries DB directly → App writes result into cache → return
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cache-aside, Java — the branching ("check, then fetch, then populate") lives in the app&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getIfPresent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;                    &lt;span class="c1"&gt;// MISS — app itself talks to the DB&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// app itself populates the cache&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; read-heavy workloads, frequently accessed data, and applications that want explicit control over caching logic (what gets cached, when, and how it's keyed).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; stale data is possible between writes — if the DB row changes but the cached copy isn't invalidated, reads serve outdated data until TTL expiry or explicit invalidation. This app-level branching logic also has to be duplicated at every call site that reads this data, unless it's wrapped in a shared helper.&lt;/p&gt;

&lt;h3&gt;
  
  
  2️⃣ Read-Through
&lt;/h3&gt;

&lt;p&gt;Similar outcome to cache-aside, but the &lt;strong&gt;loading logic lives inside the cache layer itself&lt;/strong&gt; rather than the application. Critically, the app makes &lt;strong&gt;only one outbound call, ever&lt;/strong&gt; — it never bounces back to the DB itself. On a miss, the cache's own &lt;strong&gt;configured loader&lt;/strong&gt; (registered once, at setup) fetches from the DB, stores the result, and returns it, all inside that single call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App → Cache (ONE call, period)
         Cache internally decides:
           ├─ HIT  → return from cache
           └─ MISS → cache's own loader fetches from DB → cache stores it → cache returns it
App never talks to the DB directly, in either the hit or miss case.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Read-through, Java (Caffeine) — the DB-fetch logic is registered ONCE, not per call-site&lt;/span&gt;
&lt;span class="nc"&gt;LoadingCache&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Caffeine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maximumSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// "loader" — lives inside the cache, not the app&lt;/span&gt;

&lt;span class="c1"&gt;// Every call site, hit or miss, looks identical — no branching logic in app code at all:&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt; simpler application code (no manual "check cache, then DB, then populate" boilerplate repeated everywhere), centralized caching logic, easier maintenance since the fetch-on-miss behavior lives in one place (the loader) instead of being duplicated across every call site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does Bloom filter / Cache Penetration protection go here?&lt;/strong&gt; Since the app only ever makes one call, a Bloom filter check has to sit &lt;em&gt;inside&lt;/em&gt; that single call's miss path, so it can block the loader from ever reaching the DB for an invalid key — either as a guard right before &lt;code&gt;cache.get()&lt;/code&gt; in app code, or (cleaner) pushed one layer deeper, directly inside the loader function itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;LoadingCache&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Caffeine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;bloomFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mightContain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// definitely invalid — skip DB entirely&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cache-Aside vs. Read-Through, in one line:&lt;/strong&gt; in cache-aside the &lt;em&gt;app&lt;/em&gt; owns the "check → miss → fetch → populate" sequence (two calls out); in read-through the &lt;em&gt;cache&lt;/em&gt; owns that entire sequence behind a single call, with the DB-fetch logic registered once as a loader instead of repeated per call-site.&lt;/p&gt;

&lt;h3&gt;
  
  
  3️⃣ Write-Through
&lt;/h3&gt;

&lt;p&gt;Every write goes to the &lt;strong&gt;cache first, then synchronously to the database&lt;/strong&gt;, before the write is considered complete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt; strong cache consistency — the cache always contains the latest data, since a write never returns success without also having landed in the DB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; higher write latency, since every write pays the cost of two synchronous operations instead of one.&lt;/p&gt;

&lt;h3&gt;
  
  
  4️⃣ Write-Back (Write-Behind)
&lt;/h3&gt;

&lt;p&gt;Write to the cache immediately and return; the database update happens &lt;strong&gt;asynchronously&lt;/strong&gt; afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt; extremely fast writes (the caller doesn't wait on the DB), reduced database load (writes can be batched/coalesced before flushing).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; potential data loss if the process crashes after the cache write but before the async DB flush completes.&lt;/p&gt;

&lt;h3&gt;
  
  
  5️⃣ Write-Around
&lt;/h3&gt;

&lt;p&gt;Writes &lt;strong&gt;bypass the cache entirely&lt;/strong&gt; and go directly to the database. The cache is only populated later, when that data happens to be read (effectively falling back to cache-aside on the read path).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; write-heavy systems, and data that's written often but &lt;strong&gt;rarely read&lt;/strong&gt; — e.g., logs, audit trails, bulk-ingested records. Helps avoid &lt;strong&gt;cache pollution&lt;/strong&gt;: without write-around, every write would occupy cache space for data that may never actually be read again, evicting genuinely hot data to make room for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  6️⃣ Refresh-Ahead
&lt;/h3&gt;

&lt;p&gt;Popular ("hot") data is &lt;strong&gt;proactively refreshed before it expires&lt;/strong&gt;, rather than waiting for a request to miss and trigger a reload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt; lower cache miss rate, consistently low latency (users never hit the "cold" path for data the system already predicted would still be needed), better overall experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commonly used for:&lt;/strong&gt; product catalogs, trending content, dashboard data — anything with predictable, sustained popularity where "refresh proactively" is cheaper than "let it expire and pay for a stampede of misses" (see pattern #7).&lt;/p&gt;

&lt;h3&gt;
  
  
  7️⃣ Cache Stampede Protection (Thundering Herd)
&lt;/h3&gt;

&lt;p&gt;What happens when a hot cache key expires and &lt;strong&gt;thousands of requests hit the database simultaneously&lt;/strong&gt;? That's a cache stampede (also called &lt;strong&gt;"Cache Breakdown"&lt;/strong&gt; — see "The 3 Cache Killers" section below for how this compares to Cache Penetration and Cache Avalanche).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key expires → 10,000 requests miss at the same moment → 10,000 identical queries hit the DB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common solutions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distributed locks&lt;/strong&gt; — only one process is allowed to recompute/refill the key; others wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request coalescing (single-flight)&lt;/strong&gt; — concurrent requests for the same missing key share one in-flight recomputation instead of each triggering their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staggered expiration times (jitter)&lt;/strong&gt; — add randomness to TTLs so a large batch of keys set at the same time doesn't all expire in the same instant.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8️⃣ Distributed Cache
&lt;/h3&gt;

&lt;p&gt;Multiple application instances share a &lt;strong&gt;centralized cache&lt;/strong&gt; rather than each maintaining its own local, inconsistent in-process cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt; horizontal scalability (add more app instances without duplicating cache state per instance), shared cached data (one instance's cache-fill benefits every other instance), consistent performance across nodes (no "cold" instance behind a load balancer that hasn't warmed its own local cache yet).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Redis, Memcached, Hazelcast.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 3 Cache Killers — How Redis Crashes Your Database
&lt;/h2&gt;

&lt;p&gt;Caching looks simple on paper: check cache → hit, return; miss → fetch from DB, populate cache. This works fine until traffic hits scale. &lt;strong&gt;At scale, databases rarely die from bugs — they die from cache failures.&lt;/strong&gt; These are the three named failure modes worth knowing cold:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cache Penetration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Users repeatedly request data that &lt;strong&gt;doesn't exist&lt;/strong&gt; in either the cache or the DB (e.g., &lt;code&gt;userId = -1&lt;/code&gt;, or a scraper probing random IDs). Since the data doesn't exist, it can never be cached — so every request is a guaranteed cache miss &lt;em&gt;and&lt;/em&gt; a guaranteed DB miss, forever. If this is automated/attacked at volume, it's effectively an unthrottled DB query flood with no cache layer able to absorb any of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bloom filter&lt;/strong&gt; in front of the cache/DB — a probabilistic structure that can definitively say "this key does &lt;em&gt;not&lt;/em&gt; exist" before ever touching the DB, blocking invalid keys cheaply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache the empty/negative result&lt;/strong&gt; for a short TTL (e.g., &lt;code&gt;null&lt;/code&gt; sentinel with 30-60s TTL) — so repeated requests for the same nonexistent key hit the cache (returning "not found") instead of re-querying the DB every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Cache Breakdown (Hot Key Expiry) — same as "Cache Stampede," pattern #7 above
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; A single extremely popular key (a viral profile, a trending product) is read by millions of requests. The instant that one key expires, &lt;strong&gt;all&lt;/strong&gt; of those requests miss simultaneously and hit the DB together — a single hot key becomes a synchronized flood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixes:&lt;/strong&gt; (see pattern #7 for full detail) mutex/distributed lock so only one request recomputes the value while others wait, or &lt;strong&gt;logical/soft expiration&lt;/strong&gt; — serve the stale value immediately while refreshing it in the background, so no caller ever waits on a synchronous DB hit.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cache Avalanche
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; &lt;strong&gt;Many different keys&lt;/strong&gt; expire at (approximately) the same moment — e.g., they were all set with the same TTL at the same time, or the cache server/cluster restarts and loses everything at once. Unlike breakdown (one hot key), this is a mass, simultaneous expiry across the whole keyset, flooding the DB with a broad wave of misses all at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TTL jitter&lt;/strong&gt; — randomize expiry times (e.g., &lt;code&gt;baseTTL + random(0, 60s)&lt;/code&gt;) so a batch of keys set together doesn't all die in the same instant.&lt;/li&gt;
&lt;li&gt;For the "cache cluster restart" variant: warm the cache before routing traffic to it, and/or run the cache in a replicated/HA setup so a single node restart doesn't wipe the whole dataset.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quick Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Killer&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Primary Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Penetration&lt;/td&gt;
&lt;td&gt;Requests for data that never existed&lt;/td&gt;
&lt;td&gt;Any single (invalid) key, repeatedly&lt;/td&gt;
&lt;td&gt;Bloom filter + cache negative results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breakdown / Stampede&lt;/td&gt;
&lt;td&gt;One hot key expires&lt;/td&gt;
&lt;td&gt;One key, many concurrent requests&lt;/td&gt;
&lt;td&gt;Mutex lock or logical/soft expiration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avalanche&lt;/td&gt;
&lt;td&gt;Many keys expire together (or cache restarts)&lt;/td&gt;
&lt;td&gt;Whole keyset at once&lt;/td&gt;
&lt;td&gt;TTL jitter + cache warming/HA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The meta-lesson:&lt;/strong&gt; good caching design isn't about the happy path — it's about explicitly naming what breaks at scale and how you'd catch it &lt;em&gt;before&lt;/em&gt; it takes down the database, not after.&lt;/p&gt;




&lt;h2&gt;
  
  
  Eviction Policies
&lt;/h2&gt;

&lt;p&gt;Independent of &lt;em&gt;which&lt;/em&gt; pattern above you use, once the cache is full, something has to decide what gets removed to make room:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LRU (Least Recently Used)&lt;/strong&gt; — the default, works for most cases. Evicts whatever hasn't been accessed in the longest time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LFU (Least Frequently Used)&lt;/strong&gt; — better when access patterns are &lt;strong&gt;stable&lt;/strong&gt; over time (the same items stay popular consistently). Evicts based on access &lt;strong&gt;count&lt;/strong&gt;, not recency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; for a product catalog, &lt;strong&gt;LFU&lt;/strong&gt; tends to be the better fit. Popular products get accessed consistently over long periods — a bestseller shouldn't get evicted just because there was a brief lull in requests for it, which is exactly the failure mode LRU is prone to for this access pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Questions a Caching Design Should Answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache-Aside vs. Read-Through — when would you choose each?&lt;/strong&gt; Cache-aside when you want explicit application-level control over what/how to cache (e.g., custom key composition, partial object caching); read-through when you want simpler, centralized application code and are fine with the cache layer's built-in loader behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you handle stale data?&lt;/strong&gt; Short, workload-appropriate TTLs; explicit invalidation on write (delete-or-update the cache entry when the source-of-truth changes); versioned cache keys; accept and bound staleness explicitly where it's tolerable (e.g., "eventually consistent within 30 seconds" for a dashboard) rather than pretending the cache is always perfectly fresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is a cache stampede?&lt;/strong&gt; See pattern #7 above — a hot key's expiry causing a burst of simultaneous cache misses that all hit the database at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How would you design cache invalidation?&lt;/strong&gt; Options: TTL-based (simplest, bounded staleness), event-driven invalidation (the write path explicitly deletes/updates the affected cache key(s), e.g., via a CDC stream or direct call), or versioned/keyed-by-content approaches (the cache key itself encodes a version, so an old version simply stops being requested rather than needing active eviction). "There are only two hard things in computer science: cache invalidation and naming things" — worth acknowledging this is a genuinely hard problem, not a solved one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you maintain cache consistency?&lt;/strong&gt; Pick the pattern that matches your consistency needs: write-through for strong consistency (slower writes), write-behind/cache-aside for eventual consistency (faster, but a staleness window exists) — and be explicit about which one you're trading for which.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis vs. Memcached?&lt;/strong&gt; Redis supports richer data structures (lists, sets, sorted sets, hashes, streams), persistence (AOF/RDB), replication, and pub/sub — useful when the cache needs to do more than store flat key-value pairs. Memcached is simpler, purely in-memory, multi-threaded per instance, and can have a lower per-operation overhead for pure key-value caching at very high throughput when none of Redis's extra features are needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How would you cache data for millions of users?&lt;/strong&gt; Use a &lt;strong&gt;distributed cache&lt;/strong&gt; (pattern #8) with consistent hashing to shard keys across cache nodes, size the cluster based on hot working-set size (not total data size), apply an eviction policy suited to the access pattern, and add stampede protection for any keys shared across large numbers of users (e.g., a global config or a trending list) since those are exactly the keys most likely to cause a thundering herd.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Example: Talkdesk's Dual-Store Architecture
&lt;/h2&gt;

&lt;p&gt;At Talkdesk, a &lt;strong&gt;dual-store architecture&lt;/strong&gt; was used: &lt;strong&gt;DynamoDB&lt;/strong&gt; for key-value storage, &lt;strong&gt;Elasticsearch&lt;/strong&gt; for full-text search, and &lt;strong&gt;Redis&lt;/strong&gt; as the hot-data cache layer sitting in front of both. The point: &lt;strong&gt;the access pattern drove every technology choice&lt;/strong&gt; — DynamoDB for point-lookup-style key-value access, Elasticsearch for flexible/full-text query needs, and Redis specifically for the subset of data that's read disproportionately often and can tolerate the cache's consistency/eviction trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Access pattern first. Technology second. Always.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Naming "Redis" is not a design decision — it's a detail that falls out of already having answered: &lt;em&gt;what is being read/written, how often, how stale can it be, and what happens when it's evicted or expires?&lt;/em&gt; Answer those first, and the caching strategy (which of the 8 patterns above, which eviction policy, TTL length, stampede protection) follows naturally and demonstrates real design judgment.&lt;/p&gt;

</description>
      <category>techlettersbyteja</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>The N+1 Query Problem — "It works locally, but is extremely slow in production"</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Fri, 16 Oct 2026 04:19:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/the-n1-query-problem-it-works-locally-but-is-extremely-slow-in-production-1epl</link>
      <guid>https://dev.to/tejaswipandava/the-n1-query-problem-it-works-locally-but-is-extremely-slow-in-production-1epl</guid>
      <description>&lt;p&gt;&lt;strong&gt;The scenario:&lt;/strong&gt; &lt;em&gt;Your API loads 100 users and their orders. It works locally — but becomes extremely slow in production. What could be causing it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One of the most common (and sneaky) causes: the &lt;strong&gt;N+1 Query Problem&lt;/strong&gt;. 🐢&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is It?
&lt;/h2&gt;

&lt;p&gt;Suppose the application needs to display 100 users along with their orders. You'd expect the database to fetch everything together in one efficient round-trip. But here's what often actually happens:&lt;/p&gt;

&lt;p&gt;1️⃣ The application sends &lt;strong&gt;one query&lt;/strong&gt; to fetch all 100 users.&lt;br&gt;
2️⃣ It then sends &lt;strong&gt;one additional query per user&lt;/strong&gt; to fetch that user's orders.&lt;/p&gt;

&lt;p&gt;Result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 1 query to fetch the users&lt;/li&gt;
&lt;li&gt;❌ 100 additional queries — one per user — to fetch their orders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's &lt;strong&gt;101 database queries for a single API request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the textbook &lt;strong&gt;N+1 Query Problem&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;1 query&lt;/strong&gt; fetches the parent records, then &lt;strong&gt;N additional queries&lt;/strong&gt; fetch the related records&lt;br&gt;
individually — one per parent row.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With 100 users, this might go unnoticed in a local test with a fast local DB and no network latency. With &lt;strong&gt;10,000 users&lt;/strong&gt;, the same code path triggers &lt;strong&gt;10,001 queries&lt;/strong&gt; — and it becomes catastrophic in production, especially over a real network to a real database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Hurts
&lt;/h2&gt;

&lt;p&gt;Every additional query introduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network round-trip time (multiplied by N)&lt;/li&gt;
&lt;li&gt;Database connection pool usage (queries competing for limited connections)&lt;/li&gt;
&lt;li&gt;Query-processing/parsing overhead per call&lt;/li&gt;
&lt;li&gt;More load on the database server&lt;/li&gt;
&lt;li&gt;Significantly higher API response time — often the actual root cause of "works locally, slow in prod," since local dev DBs have near-zero latency per query while production round-trips add up fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does It Happen?
&lt;/h2&gt;

&lt;p&gt;Most commonly with an &lt;strong&gt;ORM using lazy loading&lt;/strong&gt;. The application loads the users first (one query). Later, when code accesses &lt;code&gt;user.getOrders()&lt;/code&gt; inside a loop, the ORM silently and transparently issues &lt;strong&gt;another database query per user&lt;/strong&gt; at that exact point — invisible in the code (it just looks like a normal getter call), which is exactly why it's easy to miss in code review and easy to reproduce only under realistic data volumes.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Fix It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔗 1. Fetch related data together (join fetch)
&lt;/h3&gt;

&lt;p&gt;Use a join or fetch strategy that retrieves the parent (users) and the related child data (orders) together, in a single query, instead of triggering a separate query per parent row.&lt;/p&gt;

&lt;h3&gt;
  
  
  📦 2. Use batch fetching
&lt;/h3&gt;

&lt;p&gt;Instead of loading orders one user at a time, retrieve the orders for &lt;strong&gt;multiple users in one batch query&lt;/strong&gt; (e.g., &lt;code&gt;WHERE user_id IN (1, 2, 3, ..., 100)&lt;/code&gt;). This can turn 100 queries into a small handful — often reducing hundreds of queries down to just a couple.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 3. Fetch only the required fields (projections)
&lt;/h3&gt;

&lt;p&gt;If the API only needs a user's name, order number, and status, don't load the complete &lt;code&gt;User&lt;/code&gt;,&lt;code&gt;Order&lt;/code&gt;, and every related entity graph. Project only the fields actually needed for the response — less data over the wire, less mapping overhead, and often avoids triggering lazy-loaded associations entirely. — less data over the wire, less mapping overhead, and often avoids triggering lazy-loaded associations entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  📄 4. Add pagination
&lt;/h3&gt;

&lt;p&gt;Never load thousands of users (and their orders) in a single request. Retrieve a bounded page and only the data needed for that page — this caps the worst-case query count and payload size regardless of how large the underlying table grows.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do You Detect the N+1 Problem?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable database query logging&lt;/strong&gt; (e.g., Hibernate's &lt;code&gt;show-sql&lt;/code&gt; / &lt;code&gt;format_sql&lt;/code&gt;, or the database's own slow-query log) and actually read what gets generated for a given endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor the number of queries per API request&lt;/strong&gt; — a sudden spike proportional to the result set size (e.g., queries scale linearly with the number of users returned) is the classic signature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use an APM tool&lt;/strong&gt; (e.g., New Relic, Datadog, Elastic APM) — most surface a per-request query count and flag "N+1 detected" patterns automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for repeated queries with different IDs&lt;/strong&gt; in the log — the same &lt;code&gt;SELECT ... FROM orders WHERE user_id = ?&lt;/code&gt; template repeated 100 times with different parameter values is the smoking gun.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test using realistic data volumes&lt;/strong&gt; — this bug is invisible with 3 test users locally and becomes an incident with 10,000 users in production; load/integration tests need production-like data sizes to catch it before release.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Spring Boot / JPA: Which Fix Should You Prefer?
&lt;/h2&gt;

&lt;p&gt;A common follow-up question: &lt;em&gt;"Which approach do you prefer in Spring Boot — fetch joins, entity graphs, batch fetching, or DTO projections?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There isn't a single universally-correct answer — it depends on the access pattern — but here's how to reason about the trade-offs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;th&gt;Watch out for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Fetch Join&lt;/strong&gt; (&lt;code&gt;JOIN FETCH&lt;/code&gt; in JPQL)&lt;/td&gt;
&lt;td&gt;Single SQL query joins parent + child in one round-trip&lt;/td&gt;
&lt;td&gt;You always need the child collection alongside the parent for this query&lt;/td&gt;
&lt;td&gt;Fetching &lt;strong&gt;multiple&lt;/strong&gt; collections with &lt;code&gt;JOIN FETCH&lt;/code&gt; in one query causes a Cartesian product explosion (rows multiply); JPA restricts fetching more than one &lt;code&gt;List&lt;/code&gt;-type collection this way&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Entity Graph&lt;/strong&gt; (&lt;code&gt;@EntityGraph&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Declaratively specifies which associations to eagerly fetch, per query/method, without changing the entity's default fetch type globally&lt;/td&gt;
&lt;td&gt;You want eager fetching for &lt;em&gt;specific&lt;/em&gt; use cases but keep lazy loading as the sane default elsewhere&lt;/td&gt;
&lt;td&gt;Still one query per graph, but multiple collections in the same graph can hit the same Cartesian-product issue as fetch joins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Batch Fetching&lt;/strong&gt; (&lt;code&gt;@BatchSize&lt;/code&gt;, or &lt;code&gt;hibernate.default_batch_fetch_size&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Lazy loading still triggers on access, but Hibernate batches the &lt;em&gt;next&lt;/em&gt; N pending lazy loads into one &lt;code&gt;IN (...)&lt;/code&gt; query instead of one-by-one&lt;/td&gt;
&lt;td&gt;You have unavoidable lazy access patterns (e.g., generic repository code used in many contexts) and want a broad, low-effort mitigation&lt;/td&gt;
&lt;td&gt;Doesn't eliminate extra queries entirely, just batches them — still more than a single join, though far better than true N+1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;DTO Projections&lt;/strong&gt; (JPQL constructor expressions, Spring Data interface projections, or a mapper)&lt;/td&gt;
&lt;td&gt;Query selects only the exact fields needed directly into a DTO — no entity graph traversal at all&lt;/td&gt;
&lt;td&gt;Read-only / API-response use cases where you don't need full entity objects or their lazy associations&lt;/td&gt;
&lt;td&gt;Loses JPA entity features (dirty checking, lazy nav) — but for a read API, you don't want those anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Default preference:&lt;/strong&gt; start with &lt;strong&gt;DTO projections&lt;/strong&gt; for read-heavy API endpoints — they sidestep the N+1 problem entirely because there's no entity graph to lazily traverse in the first place, and they also naturally satisfy "fetch only the required fields" (fix #3) at the same time. Reserve &lt;strong&gt;fetch joins / entity graphs&lt;/strong&gt; for cases where you genuinely need full entity objects (e.g., you're about to modify and save them), and use &lt;strong&gt;batch fetching&lt;/strong&gt; as a safety net for lazy-loaded associations you can't easily eliminate across a large, shared codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Designing APIs for Massive Datasets&lt;/code&gt; — takes pagination (fix #4) and field projection (fix #3) further for very large datasets: cursor-based pagination, sparse fields, DB-level filtering, caching, and compression.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Caching Strategies&lt;/code&gt; — caching read-heavy query results to avoid re-paying the query cost at all.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>performance</category>
      <category>sql</category>
    </item>
    <item>
      <title>Optimistic Locking vs. Pessimistic Locking — Handling Concurrent Updates in SQL Databases</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Thu, 15 Oct 2026 04:15:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/optimistic-locking-vs-pessimistic-locking-handling-concurrent-updates-in-sql-databases-5128</link>
      <guid>https://dev.to/tejaswipandava/optimistic-locking-vs-pessimistic-locking-handling-concurrent-updates-in-sql-databases-5128</guid>
      <description>&lt;p&gt;What happens when two users try to update the same database row at exactly the same time? Without proper concurrency control, one update can silently overwrite the other, leading to inconsistent or incorrect data (the classic &lt;strong&gt;lost update&lt;/strong&gt; problem). SQL databases solve this with two main concurrency control strategies: &lt;strong&gt;Optimistic Locking&lt;/strong&gt; and &lt;strong&gt;Pessimistic Locking&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Optimistic Locking (Version-Based)
&lt;/h2&gt;

&lt;p&gt;Instead of locking the row up front, each transaction reads the current &lt;strong&gt;version number&lt;/strong&gt; along with the data. When updating, the database verifies the version hasn't changed since it was read:&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;accounts&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If another transaction already modified the row (and bumped the version), this &lt;code&gt;UPDATE&lt;/code&gt; affects &lt;strong&gt;zero rows&lt;/strong&gt; — the version check fails immediately, and the application detects the conflict via the affected-row count and can retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✔ No row-level lock held while "thinking" — the row stays fully available to others.&lt;/li&gt;
&lt;li&gt;✔ Better throughput for read-heavy systems, since nothing blocks concurrent readers/writers.&lt;/li&gt;
&lt;li&gt;✔ Conflicts are detected at &lt;code&gt;UPDATE&lt;/code&gt; time, not up front.&lt;/li&gt;
&lt;li&gt;✔ The application is responsible for retrying on failure (re-read the row, reapply the change, try again).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mental model:&lt;/strong&gt; &lt;em&gt;"Try first. If someone already changed the data, fail fast and retry."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔒 Pessimistic Locking (Row-Level Lock)
&lt;/h2&gt;

&lt;p&gt;Before modifying data, the transaction explicitly locks the row so no one else can touch it:&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;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;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While one transaction holds the lock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Other transactions requesting the same row &lt;strong&gt;wait in a queue&lt;/strong&gt; rather than failing.&lt;/li&gt;
&lt;li&gt;They execute only after the lock is released (commit/rollback).&lt;/li&gt;
&lt;li&gt;No version conflict ever occurs, because updates to that row are forced to happen &lt;strong&gt;sequentially&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✔ Row locked immediately — guarantees exclusive access for the duration of the transaction.&lt;/li&gt;
&lt;li&gt;✔ Other requests wait instead of failing outright.&lt;/li&gt;
&lt;li&gt;✔ Strong consistency, even under heavy contention on the same rows.&lt;/li&gt;
&lt;li&gt;✔ Lower concurrency (more waiting/blocking) but highly predictable execution order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mental model:&lt;/strong&gt; &lt;em&gt;"Wait your turn before making changes."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📌 When Should You Use Each?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose Optimistic Locking when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conflicts are rare.&lt;/li&gt;
&lt;li&gt;The application is read-heavy.&lt;/li&gt;
&lt;li&gt;High throughput matters more than avoiding occasional retries.&lt;/li&gt;
&lt;li&gt;Retries are acceptable at the application layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Examples: e-commerce product catalogs, user profiles, content management systems.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose Pessimistic Locking when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data integrity is critical and lost updates are unacceptable.&lt;/li&gt;
&lt;li&gt;Many users frequently update the &lt;em&gt;same&lt;/em&gt; records (high contention).&lt;/li&gt;
&lt;li&gt;Predictable, sequential execution is more valuable than raw throughput.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Examples: banking transactions, digital wallets, inventory management, ticket booking systems.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Key Takeaway
&lt;/h2&gt;

&lt;p&gt;There isn't a universally "better" approach. The right choice depends on your application's &lt;strong&gt;contention level, consistency requirements, throughput goals, and user experience&lt;/strong&gt;. Understanding this trade-off is essential for designing scalable, reliable, high-performance backend systems — it's a decision made per use case (or even per table/operation), not a single global choice for the whole system.&lt;/p&gt;

&lt;p&gt;In practice, each strategy carries a distinct failure mode worth watching for in production:&lt;br&gt;
&lt;strong&gt;retry storms&lt;/strong&gt; under optimistic locking when contention is higher than expected, and &lt;strong&gt;lock contention or deadlocks&lt;/strong&gt; under pessimistic locking when transactions hold locks longer than intended.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>Bloom Filters — The Probabilistic "Definitely Not" Gate</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Wed, 14 Oct 2026 04:11:46 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/bloom-filters-the-probabilistic-definitely-not-gate-4aee</link>
      <guid>https://dev.to/tejaswipandava/bloom-filters-the-probabilistic-definitely-not-gate-4aee</guid>
      <description>&lt;p&gt;A Bloom filter answers exactly one question, cheaply, at massive scale: &lt;strong&gt;"could this key possibly exist?"&lt;/strong&gt; It can say &lt;strong&gt;"definitely no"&lt;/strong&gt; with 100% certainty, or &lt;strong&gt;"maybe yes — go check for real."&lt;/strong&gt; It never says "yes" with certainty, and that asymmetry is the entire point of the data structure.&lt;/p&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a cache. It doesn't store keys, values, or "the list of things that failed before." It stores nothing but &lt;strong&gt;bits&lt;/strong&gt; — and it's that detail that makes it so compact it can represent membership across tens of millions of keys in a few megabytes of memory.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Mechanism
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt; a fixed-size bit array (say, 1,000,000 bits, all initialized to &lt;code&gt;0&lt;/code&gt;) plus a small number of independent hash functions (commonly 3–7).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Insert a key&lt;/strong&gt; (done once, for every &lt;em&gt;valid&lt;/em&gt; key that exists — e.g., every real &lt;code&gt;userId&lt;/code&gt; in the DB): run the key through each hash function, each one produces an index into the bit array, and set &lt;strong&gt;those specific bits to &lt;code&gt;1&lt;/code&gt;&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;insert("user_42"):
    hash1("user_42") = 3   → bitArray[3]   = 1
    hash2("user_42") = 57  → bitArray[57]  = 1
    hash3("user_42") = 910 → bitArray[910] = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this for every valid key up front. After millions of inserts, the bit array is a dense mix of 1s and 0s — no individual key is retrievable from it. It's a pure Yes/No oracle, not a lookup table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check a key&lt;/strong&gt; (at request time): run it through the same hash functions, and check whether &lt;strong&gt;all&lt;/strong&gt; of the resulting bit positions are currently &lt;code&gt;1&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;check("user_999999"):              // never actually inserted
    hash1("user_999999") = 3    → bitArray[3]   == 1 ✓
    hash2("user_999999") = 488  → bitArray[488] == 0 ✗   → STOP. Definitely NOT present.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;strong&gt;any&lt;/strong&gt; bit is &lt;code&gt;0&lt;/code&gt;, the key is &lt;strong&gt;guaranteed absent&lt;/strong&gt; — reject immediately, never touch the cache or DB. If &lt;strong&gt;all&lt;/strong&gt; bits are &lt;code&gt;1&lt;/code&gt;, the key is &lt;strong&gt;"probably present"&lt;/strong&gt; — but it might be a coincidence of other keys' bits overlapping (a &lt;strong&gt;false positive&lt;/strong&gt;). Proceed to the real cache/DB check to confirm.&lt;/p&gt;




&lt;h2&gt;
  
  
  False Positives vs. False Negatives
&lt;/h2&gt;

&lt;p&gt;These two failure modes are &lt;strong&gt;not symmetric&lt;/strong&gt; — one is an accepted, tunable trade-off; the other should never structurally happen, and if it does, it's a sign something's broken operationally.&lt;/p&gt;

&lt;h3&gt;
  
  
  False positives — expected, and the whole trade-off of using this structure
&lt;/h3&gt;

&lt;p&gt;Caused by &lt;strong&gt;hash collisions&lt;/strong&gt;: unrelated keys happen to set an overlapping combination of bits, so a never-inserted key can coincidentally have all its bit positions already &lt;code&gt;1&lt;/code&gt; from &lt;em&gt;other&lt;/em&gt; keys. This gets more likely as the filter fills up (higher load factor). It's mitigated, not eliminated, by sizing the bit array bigger and/or using more hash functions — a direct memory-vs-accuracy trade-off, not a bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  False negatives — should be structurally impossible, with exactly one real-world exception
&lt;/h3&gt;

&lt;p&gt;Insertion only ever &lt;strong&gt;sets&lt;/strong&gt; bits — it never clears them. So if a key was properly inserted, its bits are guaranteed to stay &lt;code&gt;1&lt;/code&gt; forever... &lt;strong&gt;unless something else clears one of them.&lt;/strong&gt; The only mechanisms that break this guarantee:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Why it happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Naive deletion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standard Bloom filters don't support deletion — clearing a bit to "remove" key A can silently break key B if B shared that bit position.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Counting Bloom Filter overflow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The deletion-capable variant uses small counters instead of single bits (increment on insert, decrement on delete). If a counter is sized too small and overflows/wraps, it can corrupt back to looking empty.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Staleness / not re-inserted on write&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The filter was built from a DB snapshot, and a &lt;em&gt;new&lt;/em&gt; valid key was created afterward but never inserted into the filter. Structurally the filter is fine — this is an operational gap, not a flaw in the structure itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Quick comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;False Positive&lt;/th&gt;
&lt;th&gt;False Negative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Possible by design?&lt;/td&gt;
&lt;td&gt;Yes — accepted trade-off&lt;/td&gt;
&lt;td&gt;No — only via deletion bugs, overflow, or staleness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consequence&lt;/td&gt;
&lt;td&gt;One extra (cheap) real check against cache/DB&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;real, existing key gets wrongly rejected&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Severity&lt;/td&gt;
&lt;td&gt;Low — self-correcting, costs one wasted lookup&lt;/td&gt;
&lt;td&gt;High — silently drops valid users/data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Sizing It Correctly
&lt;/h2&gt;

&lt;p&gt;The false-positive rate isn't a fixed property of a Bloom filter — it's something you design for upfront based on three numbers: the bit array size &lt;code&gt;m&lt;/code&gt;, the number of hash functions &lt;code&gt;k&lt;/code&gt;, and the number of items you expect to insert &lt;code&gt;n&lt;/code&gt;. Given a target false-positive rate &lt;code&gt;p&lt;/code&gt;, the standard formulas are:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;m = -(n * ln(p)) / (ln(2))^2&lt;/code&gt; to get the number of bits you need, and &lt;code&gt;k = (m / n) * ln(2)&lt;/code&gt; to get the optimal number of hash functions for that bit array.&lt;/p&gt;

&lt;p&gt;As a concrete example, to hold 10 million keys at a 1% false-positive rate, you land on roughly 9.6 million bits (about 1.2 MB) and 7 hash functions — which is the entire appeal of this structure: representing membership across 10 million keys in little more than a megabyte, instead of the hundreds of megabytes a real set of those keys would cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Lives (Placement)
&lt;/h2&gt;

&lt;p&gt;A Bloom filter is an &lt;strong&gt;in-memory data structure&lt;/strong&gt;, not a network service — it lives wherever the code that needs to make the "should I even bother checking the cache/DB?" decision is running.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;App server memory&lt;/strong&gt; (most common)&lt;/td&gt;
&lt;td&gt;Default choice — checking a local bit array is nanoseconds&lt;/td&gt;
&lt;td&gt;Each instance's filter must be independently built/kept in sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Cache server&lt;/strong&gt; (e.g., RedisBloom module, &lt;code&gt;BF.ADD&lt;/code&gt; / &lt;code&gt;BF.EXISTS&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Many app instances, want one shared/consistent filter instead of N copies&lt;/td&gt;
&lt;td&gt;Adds a network hop to check — still far cheaper than a DB hit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Gateway / Load Balancer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rare — only to block abusive/scraping traffic before it reaches any app server at all&lt;/td&gt;
&lt;td&gt;Unusual to couple business-logic membership checks into an infra layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Cache-Aside vs. Read-Through
&lt;/h3&gt;

&lt;p&gt;Where the check physically goes in your code depends on which caching pattern you're using. In &lt;strong&gt;Cache-Aside&lt;/strong&gt;, the app owns the "check cache → miss → query DB" sequence directly, so the Bloom filter check goes right before the app's own DB call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;bloomFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mightContain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// skip cache AND DB entirely&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getIfPresent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;strong&gt;Read-Through&lt;/strong&gt;, the app only ever makes &lt;em&gt;one&lt;/em&gt; call to the cache; the DB-fetch logic is hidden inside the cache's registered loader. So the Bloom filter check has to move &lt;strong&gt;inside that loader&lt;/strong&gt;, since that's the only place a DB call actually happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;LoadingCache&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Caffeine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;bloomFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mightContain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// loader skips the DB call&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Cold-Start Problem: What Happens on App Restart?
&lt;/h2&gt;

&lt;p&gt;A freshly created Bloom filter starts with &lt;strong&gt;every bit at &lt;code&gt;0&lt;/code&gt;&lt;/strong&gt;. If an app instance restarts and starts serving traffic before its filter is repopulated, &lt;code&gt;mightContain(anyKey)&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt; for &lt;strong&gt;every key — including real, valid ones.&lt;/strong&gt; This isn't a tunable false-positive trade-off anymore; it's the filter wrongly rejecting all legitimate traffic, which is worse than not having a Bloom filter at all.&lt;/p&gt;

&lt;p&gt;This has to be designed for explicitly:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Warm up before accepting traffic
&lt;/h3&gt;

&lt;p&gt;Rebuild the filter from a full scan of valid keys in the DB &lt;strong&gt;before&lt;/strong&gt; the instance passes its readiness check / joins the load balancer rotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@PostConstruct&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;warmUpBloomFilter&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;bloomFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BloomFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt;
    &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;streamAllIds&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;bloomFilter:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// only now mark this instance ready / healthy&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Persist the filter so you don't rebuild from scratch every time
&lt;/h3&gt;

&lt;p&gt;Serialize the bit array periodically to durable storage (disk, S3, or a shared store), and reload that snapshot on startup instead of re-scanning the whole DB every restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;bloomFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputStream&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                              &lt;span class="c1"&gt;// periodic snapshot&lt;/span&gt;
&lt;span class="nc"&gt;BloomFilter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BloomFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readFrom&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;funnel&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// on startup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Keep it in sync on writes — this is the part that's easy to miss
&lt;/h3&gt;

&lt;p&gt;Every time a new valid key is written to the DB, the filter has to be updated &lt;strong&gt;in the same write path&lt;/strong&gt;, or that new key will be falsely rejected until the next full rebuild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;bloomFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// keep filter in sync, same code path&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If each app instance holds its &lt;strong&gt;own independent&lt;/strong&gt; filter (the common case), a write on Instance A doesn't automatically propagate to Instances B, C, D. There are three common ways to handle that:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shared filter (RedisBloom)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;All instances read/write the same filter over the network&lt;/td&gt;
&lt;td&gt;Network hop per check; sync problem disappears entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pub/Sub broadcast&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Instance A publishes "key X added" to Kafka/Redis pub-sub; every instance updates its own local filter&lt;/td&gt;
&lt;td&gt;Local in-memory speed retained; small propagation-delay window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Periodic full rebuild&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every instance re-scans the DB on a schedule (e.g., every 10 min)&lt;/td&gt;
&lt;td&gt;Simplest; accepts a bounded staleness window by design&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The deeper rule: never let the filter become correctness-critical
&lt;/h3&gt;

&lt;p&gt;The deeper rule underneath all three options is the same: because a cold or desynced filter risks &lt;strong&gt;false negatives&lt;/strong&gt;, treat it as a pure &lt;strong&gt;optimization&lt;/strong&gt;, never as the sole gate standing between a request and a real answer. Track a &lt;code&gt;filterReady&lt;/code&gt; flag set only once warm-up completes — before that, fall back to hitting the real cache/DB for every request, accepting the temporary loss of the optimization rather than risking wrongly rejecting valid data.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How does a Bloom filter differ from a cache?&lt;/strong&gt; A cache stores actual values and can answer "yes, here's the data." A Bloom filter stores no data at all — only bits — and can only ever answer "definitely no" or "maybe." It's a filter in front of the real store, not a replacement for one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you delete from a Bloom filter?&lt;/strong&gt; Not from a standard one — clearing a bit can corrupt other keys sharing it. Use a &lt;strong&gt;Counting Bloom Filter&lt;/strong&gt; (small counters instead of single bits) if deletion is required, and size the counters generously to avoid overflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the filter fills up?&lt;/strong&gt; The false-positive rate rises as the load factor increases. Size the bit array ahead of time based on expected key count and target false-positive rate, using the sizing formulas above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the app restarts?&lt;/strong&gt; See the Cold-Start section above — warm up before serving, persist snapshots, and keep multi-instance filters in sync via a shared store or broadcast.&lt;/p&gt;

</description>
      <category>techlettersbyteja</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Backpressure — "Your system isn't crashing from too much traffic. It's crashing from too little control."</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:42:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/backpressure-your-system-isnt-crashing-from-too-much-traffic-its-crashing-from-too-little-279c</link>
      <guid>https://dev.to/tejaswipandava/backpressure-your-system-isnt-crashing-from-too-much-traffic-its-crashing-from-too-little-279c</guid>
      <description>&lt;p&gt;Most production outages aren't caused by a lack of raw compute — they're caused by the &lt;strong&gt;absence of a feedback mechanism&lt;/strong&gt; between a fast producer and a slower consumer. That mechanism is called &lt;strong&gt;backpressure&lt;/strong&gt;, and it's one of the most underrated concepts in distributed systems design.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 The Problem
&lt;/h2&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incoming traffic: &lt;strong&gt;1000 requests/sec&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Your system can actually process: &lt;strong&gt;200 requests/sec&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without any control mechanism, here's the familiar death spiral:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Traffic spikes.&lt;/li&gt;
&lt;li&gt;Latency increases (requests queue up waiting to be processed).&lt;/li&gt;
&lt;li&gt;Requests pile up in memory/queues.&lt;/li&gt;
&lt;li&gt;Clients time out and &lt;strong&gt;retry&lt;/strong&gt;, which adds &lt;em&gt;more&lt;/em&gt; load on top of the backlog — amplifying it.&lt;/li&gt;
&lt;li&gt;Thread pools / memory are exhausted.&lt;/li&gt;
&lt;li&gt;The system crashes — and takes healthy downstream services down with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The root cause isn't the 1000 RPS itself. It's that &lt;strong&gt;nobody told the producer to slow down.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 What Backpressure Actually Is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Backpressure is a feedback signal from a consumer to a producer, communicating its current capacity.&lt;/strong&gt; When the consumer is overwhelmed, it tells the producer to slow down — or stop — before the consumer collapses.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Backpressure is not about handling peak traffic. It's about surviving overload without collapsing.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With backpressure applied:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Excess load is &lt;strong&gt;rejected early&lt;/strong&gt;, at the edge, instead of silently queuing.&lt;/li&gt;
&lt;li&gt;Latency stays &lt;strong&gt;predictable&lt;/strong&gt; for accepted requests.&lt;/li&gt;
&lt;li&gt;Downstream services stay protected from cascading failure.&lt;/li&gt;
&lt;li&gt;The system degrades &lt;strong&gt;gracefully&lt;/strong&gt; instead of catastrophically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why it must be applied
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Avoid overloading downstream services&lt;/strong&gt; — upstream producers shouldn't be able to push consumers past their limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource efficiency&lt;/strong&gt; — don't burn memory/CPU buffering data you'll never actually get to process; spend those resources on work you can finish.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Real-world examples
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network communication:&lt;/strong&gt; a client sends 1000 RPS, the backend can only handle 700 RPS — requests queue up and eventually exhaust memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File processing:&lt;/strong&gt; reading from disk is usually faster than writing (or than a downstream consumer processing records) — loading a large file faster than it can be consumed causes RAM pressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pain points
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Propagation&lt;/strong&gt; — efficiently propagating a backpressure signal &lt;em&gt;across&lt;/em&gt; microservice boundaries (not just within one process) is genuinely hard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency cost&lt;/strong&gt; — backpressure mechanisms (buffering, signaling, waiting) add latency of their own. Budget for it; it's the price of stability.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🌍 Where Backpressure Shows Up
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;How backpressure is signaled&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Reactive Streams&lt;/strong&gt; (Project Reactor, RxJava)&lt;/td&gt;
&lt;td&gt;Subscriber calls &lt;code&gt;request(n)&lt;/code&gt; — it &lt;em&gt;pulls&lt;/em&gt; exactly &lt;code&gt;n&lt;/code&gt; items; the publisher cannot push more than that&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumers &lt;code&gt;poll()&lt;/code&gt; at their own pace — Kafka never pushes to consumers; &lt;strong&gt;consumer lag&lt;/strong&gt; is the backpressure signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumer sets a &lt;strong&gt;prefetch count&lt;/strong&gt; — broker only pushes that many unacknowledged messages at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Receiver advertises a &lt;strong&gt;receive window&lt;/strong&gt;; sender throttles once the window fills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP/2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stream-level and connection-level flow-control windows, same idea as TCP but per-stream&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Microservices (sync calls)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bounded thread pools / connection pools + timeouts — once exhausted, new calls fail fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;File I/O&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Disk reads are usually faster than writes/consumption — buffered readers must pause when the write/consume side falls behind&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  💡 The Two Models: Push vs Pull
&lt;/h2&gt;

&lt;p&gt;This is the single most important mental model for backpressure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Push-based&lt;/th&gt;
&lt;th&gt;Pull-based&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who drives the pace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Producer&lt;/td&gt;
&lt;td&gt;Consumer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Producer can overwhelm a slow consumer&lt;/td&gt;
&lt;td&gt;Consumer naturally throttles itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Naive message broadcast, unbounded event emission&lt;/td&gt;
&lt;td&gt;Kafka &lt;code&gt;poll()&lt;/code&gt;, Reactive Streams &lt;code&gt;request(n)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backpressure needed explicitly?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — must be bolted on&lt;/td&gt;
&lt;td&gt;Built-in by design&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most modern backpressure-aware systems (Reactive Streams, Kafka) default to &lt;strong&gt;pull-based&lt;/strong&gt; consumption specifically because it makes backpressure the natural behavior rather than an afterthought — &lt;strong&gt;processing speed defines system throughput&lt;/strong&gt;, not emission rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  A small code example (Project Reactor)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Flux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onBackpressureBuffer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                     &lt;span class="c1"&gt;// bounded buffer, not unbounded&lt;/span&gt;
        &lt;span class="n"&gt;dropped&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dropping: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;BufferOverflowStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DROP_OLDEST&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Schedulers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;boundedElastic&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subscriber implicitly signals demand via &lt;code&gt;request(n)&lt;/code&gt;; if &lt;code&gt;process()&lt;/code&gt; can't keep up, the buffer absorbs a bounded burst and then &lt;strong&gt;drops&lt;/strong&gt; rather than growing memory unboundedly.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Implementing Backpressure — Layer by Layer
&lt;/h2&gt;

&lt;p&gt;Walking the same example (1000 RPS in, 200 RPS capacity) up the stack:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. API Layer — Limit In-Flight Requests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why first:&lt;/strong&gt; the API is the entry point; if it doesn't apply backpressure, nothing downstream can be saved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior:&lt;/strong&gt; allow a fixed number of concurrent requests; reject the rest immediately with &lt;code&gt;429 Too Many Requests&lt;/code&gt; / &lt;code&gt;503 Service Unavailable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; pressure is pushed back upstream to the client; internal components stay protected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Principle:&lt;/strong&gt; &lt;em&gt;a fast failure is better than a slow success.&lt;/em&gt; A client that gets an instant 429 can retry with backoff; a client stuck waiting 30s for a timeout ties up resources on both ends.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Bounded Queues
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; unbounded queues don't prevent overload — they just &lt;strong&gt;delay&lt;/strong&gt; the failure while quietly exhausting memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; bounded queues with explicit capacity limits. When full:

&lt;ul&gt;
&lt;li&gt;stop accepting new tasks,&lt;/li&gt;
&lt;li&gt;reject or block the producer intentionally,&lt;/li&gt;
&lt;li&gt;this caps memory usage and makes overload &lt;strong&gt;visible&lt;/strong&gt; instead of hidden.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Pull-Based Consumption
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Push-based producers blast messages continuously, regardless of consumer readiness, and consumers get overwhelmed.&lt;/li&gt;
&lt;li&gt;Pull-based consumers request work only when ready — the consumer controls the pace.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Respect Downstream/DB Limits as a Signal
&lt;/h3&gt;

&lt;p&gt;Databases naturally enforce backpressure via:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;connection pool limits,&lt;/li&gt;
&lt;li&gt;row/table locks,&lt;/li&gt;
&lt;li&gt;query execution timeouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't fight these limits — &lt;strong&gt;fail fast when connections are exhausted&lt;/strong&gt;, and &lt;strong&gt;propagate the failure upstream&lt;/strong&gt; instead of retrying blindly against an already-saturated DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Load Shedding (Last Resort)
&lt;/h3&gt;

&lt;p&gt;When overloaded severely, the system should prioritize core functionality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reject low-priority requests first,&lt;/li&gt;
&lt;li&gt;disable non-critical features,&lt;/li&gt;
&lt;li&gt;serve cached/degraded responses instead of computing fresh ones (e.g., a monitoring system switching to sampling instead of capturing every event under load).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔎 How Do You Know You Need Backpressure?
&lt;/h2&gt;

&lt;p&gt;Watch for these signals before they turn into an outage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queue depth climbing&lt;/strong&gt; steadily instead of oscillating around a baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p99 latency rising&lt;/strong&gt; while p50 stays flat (a classic sign of queuing, not raw slowness).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer lag growing&lt;/strong&gt; in Kafka/queue-based systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thread pool / connection pool utilization&lt;/strong&gt; approaching 100% sustained, not just spiking.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚠️ Backpressure vs. Look-Alikes
&lt;/h2&gt;

&lt;p&gt;These get confused constantly — know the distinction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Why it's &lt;em&gt;not&lt;/em&gt; backpressure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retries&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client resends a failed request&lt;/td&gt;
&lt;td&gt;Actually &lt;strong&gt;amplifies&lt;/strong&gt; load if the root cause was overload — the opposite of relief&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Autoscaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adds more capacity reactively&lt;/td&gt;
&lt;td&gt;Reacts &lt;strong&gt;too slowly&lt;/strong&gt; (minutes) for a traffic spike that happens in seconds; doesn't stop the immediate overload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Caps requests per client/key ahead of time&lt;/td&gt;
&lt;td&gt;A &lt;em&gt;static, predefined&lt;/em&gt; cap — not a dynamic feedback signal based on real-time consumer capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Circuit breaker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stops calling a failing dependency entirely&lt;/td&gt;
&lt;td&gt;Binary open/closed based on failure rate — not a graduated capacity signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backpressure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dynamic, continuous signal from consumer capacity back to producer&lt;/td&gt;
&lt;td&gt;The only one of these that's a true closed feedback loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🎯 Key Takeaway
&lt;/h2&gt;

&lt;p&gt;A common misconception:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❌ "Backpressure means rate limiting requests."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A more precise way to think about it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ "Backpressure is a feedback loop where a consumer signals its real-time capacity back to the producer, so the producer can slow down, buffer within bounds, or shed load — before the consumer is overwhelmed. Rate limiting is a static, predefined cap; backpressure is dynamic and capacity-aware, and is typically implemented via bounded queues, pull-based consumption (Kafka, Reactive Streams), or explicit rejection at the entry point."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The distinction matters because it's a &lt;strong&gt;control-loop&lt;/strong&gt; concept, not just a synonym for throttling.&lt;/p&gt;

</description>
      <category>techlettersbyteja</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>10 Must-Know System Design Failure Modes</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Sun, 11 Oct 2026 09:46:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/10-must-know-system-design-failure-modes-5chd</link>
      <guid>https://dev.to/tejaswipandava/10-must-know-system-design-failure-modes-5chd</guid>
      <description>&lt;p&gt;It's easy to explain how a system works when everything goes right. The harder — and more valuable — question is: what happens when it does &lt;strong&gt;not&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Distributed systems don't fail in infinite ways — they fail in a small number of recognizable patterns, and the same ones keep appearing in postmortems at every company. Once you can name a pattern, you can design against it before it ever becomes an incident. None of these need exotic infrastructure to hit — most show up the first time a system carries real traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Single Point of Failure
&lt;/h2&gt;

&lt;p&gt;Any component on the request path with &lt;strong&gt;no redundancy&lt;/strong&gt; — one load balancer, one primary database, one message broker, one auth service. When it dies, everything behind it becomes unreachable, even though those instances are perfectly healthy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clients ──► [Load Balancer] ──► App Server 1, 2, 3 (all healthy)
                  ✗ dies
Clients ──► ✗ ──► unreachable, despite 3 healthy app servers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Redundancy + automatic failover. Run at least two instances of everything on the critical path, put health checks in front of them, and spread instances across availability zones so one datacenter problem can't remove an entire tier. Failover must be &lt;strong&gt;tested&lt;/strong&gt; — a standby that's never taken traffic is a guess, not a plan.&lt;/p&gt;

&lt;p&gt;A useful habit when reviewing any architecture: trace each request path and ask which single box, if deleted, stops all traffic. That's the box to fix first.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cascading Failure
&lt;/h2&gt;

&lt;p&gt;One slow dependency rarely stays contained. A database slows from 20ms to 5s → the service calling it holds its (fixed-size) thread pool open waiting → it stops answering its own callers → they block too. The slowness travels &lt;strong&gt;backwards&lt;/strong&gt; up the call chain until the whole system looks down, even though only one component actually degraded.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → Service A → Service B → Service C → DB (slow: 20ms → 5s)
                                     ↑ threads held open
                          ↑ B's threads exhausted, stops responding
             ↑ A's threads exhausted, stops responding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Containment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aggressive timeouts&lt;/strong&gt; — a caller gives up rather than waiting forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breakers&lt;/strong&gt; — stop sending traffic to a dependency that's already failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulkheads&lt;/strong&gt; — give each dependency its own connection pool, so one sick downstream can't consume every thread the service owns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Retry Storm
&lt;/h2&gt;

&lt;p&gt;Retries are usually good — they stop being good when everyone retries at the same moment. A service comes back after a blip; every client that failed during the outage retries at once, often multiple times each. The recovering service now sees several times its normal load, falls over again, generating a fresh round of retries. &lt;strong&gt;The retry logic becomes the outage.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 clients × 3 retries each = 9x load on a service that just barely recovered → falls over again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exponential backoff with jitter&lt;/strong&gt; — spreads attempts across a window instead of synchronizing on the same second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry budget&lt;/strong&gt; — caps retries as a percentage of total traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circuit breaker&lt;/strong&gt; — clients stop retrying a service that's clearly down.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Cache Stampede
&lt;/h2&gt;

&lt;p&gt;Caches hide how much load your database really receives. When one popular key expires, every request that would have been a cache hit becomes a miss &lt;strong&gt;at the same instant&lt;/strong&gt;. All of them run the same expensive query against the database simultaneously — which suddenly takes the full read volume it's been shielded from for months.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cache key expires → 1,000 concurrent requests all miss → all 1,000 hit the DB with the same query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request coalescing (single-flight)&lt;/strong&gt; — the first miss recomputes the value; the rest wait on that in-flight result instead of each triggering their own DB query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh hot keys shortly before expiry&lt;/strong&gt; — keeps them warm, avoiding the miss entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add jitter to TTLs&lt;/strong&gt; — stops thousands of keys from expiring together in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Hot Partition
&lt;/h2&gt;

&lt;p&gt;Sharding assumes traffic spreads evenly across shards. Real traffic doesn't. Pick a &lt;strong&gt;low-cardinality shard key&lt;/strong&gt; (country, tenant, day) and one shard receives most of the writes while others idle. That shard saturates first — and adding more shards doesn't help, because the skew follows the &lt;em&gt;key&lt;/em&gt;, not the machine count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shard key = country → Shard 1 (US): 92% of writes | Shard 2, 3: idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose a &lt;strong&gt;high-cardinality, evenly-distributed&lt;/strong&gt; key — e.g., a hash of the user ID.&lt;/li&gt;
&lt;li&gt;For genuinely "celebrity" entities that are hot no matter what: &lt;strong&gt;salt the key into sub-partitions&lt;/strong&gt;, or give those entities dedicated capacity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track per-shard metrics&lt;/strong&gt;, not cluster averages — an average hides the one node that's on fire.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Replication Lag
&lt;/h2&gt;

&lt;p&gt;Read replicas make reads cheap, but they're always slightly behind the primary. Under load, that gap grows from milliseconds to seconds. A user updates their profile, the write commits on the primary, the next read lands on a lagging replica — the change appears to have vanished. Nothing is technically broken, but the bug report is still real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client writes → Primary (committed) → Replica (3s behind) → Client reads → gets STALE value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Be explicit about which reads can tolerate staleness.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-your-writes routing&lt;/strong&gt; — route to the primary for a short window after a write by that same user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track replication position&lt;/strong&gt; — only serve a read from a replica that has caught up past the write's position.&lt;/li&gt;
&lt;li&gt;Analytics/feed reads can safely stay on replicas — they don't need freshness.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Duplicate Processing
&lt;/h2&gt;

&lt;p&gt;Any retried request may execute twice. The first attempt reaches the server and succeeds, the response is lost to a timeout, and the client retries the same operation. For a read, harmless. For a &lt;strong&gt;payment, an email send, or an inventory decrement&lt;/strong&gt;, the user is charged twice and the ledger is wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → charge $100 → Server processes, charges $100 → response lost (timeout)
Client retries → charge $100 → Server charges $100 AGAIN → $200 total charged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix: Idempotency.&lt;/strong&gt; Client attaches a unique &lt;strong&gt;idempotency key&lt;/strong&gt; to each logical operation; server records that key with the result. When a request arrives carrying a key it's already processed, the server skips the work and returns the original response — same request, same outcome, any number of attempts.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Queue Backlog
&lt;/h2&gt;

&lt;p&gt;Queues absorb bursts, which is exactly why they hide problems. If producers publish faster than consumers process — even by a small margin — the backlog grows continuously. Every health check stays green while the age of the oldest message climbs from seconds to hours, and by the time anyone notices, the data being processed is badly out of date.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producers: 10,000 msgs/sec  |  Consumers: 2,000 msgs/sec  →  queue grows by 8,000/sec, forever
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Make the imbalance visible and bounded.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alert on consumer lag and oldest-message age&lt;/strong&gt;, not just queue length alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Autoscale consumers on lag.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bound the queue&lt;/strong&gt; so it applies backpressure, and &lt;strong&gt;shed low-value work&lt;/strong&gt; under sustained overload instead of accepting everything and falling further behind.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Poison Message
&lt;/h2&gt;

&lt;p&gt;One malformed message can stop an entire queue. The consumer picks it up, throws, and never acknowledges it. The broker redelivers it, the consumer throws again, and the loop repeats &lt;strong&gt;forever&lt;/strong&gt;. Throughput drops to zero while every valid message behind the bad one waits — and the crash loop can take the whole consumer group down with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[bad msg] [msg2] [msg3] ... → consumer crashes on bad msg → redelivered → crashes again → forever
                                                                              ↑ msg2, msg3... never processed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cap delivery attempts&lt;/strong&gt;, then move the message to a &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; so the consumer can continue past it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alert on anything landing in the DLQ.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Keep enough context on the message to fix the underlying bug and &lt;strong&gt;replay it later&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Split Brain
&lt;/h2&gt;

&lt;p&gt;During a network partition, &lt;strong&gt;two nodes can both conclude they are the primary.&lt;/strong&gt; Each accepts writes from the clients that can still reach it, and the two copies of the data diverge. When the partition heals, there's no correct way to merge them — both sides accepted writes assuming they were authoritative.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Network partition:
  Node A (thinks it's primary) ← writes from Client 1
  Node B (thinks it's primary) ← writes from Client 2
  → data diverges, no clean merge once partition heals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Make leadership require a &lt;strong&gt;majority&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quorum-based writes&lt;/strong&gt; — only the side holding more than half the nodes can commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consensus-based leader election&lt;/strong&gt; (e.g., Raft) — prevents two leaders being elected in the same term.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fencing tokens&lt;/strong&gt; — let storage reject writes from a leader whose lease has already expired.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Patterns Worth Pairing Together
&lt;/h2&gt;

&lt;p&gt;A few of these failure modes naturally show up as a package when you propose a design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a cache → think about &lt;strong&gt;stampede protection&lt;/strong&gt; up front.&lt;/li&gt;
&lt;li&gt;Add sharding → name your &lt;strong&gt;shard key&lt;/strong&gt; and why it won't go hot.&lt;/li&gt;
&lt;li&gt;Add retries → pair them with &lt;strong&gt;backoff and jitter&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add replicas → decide &lt;strong&gt;which reads can tolerate lag&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anyone can draw boxes on a diagram — the useful skill is explaining how those boxes behave &lt;strong&gt;on the worst day of the quarter&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Failure Mode&lt;/th&gt;
&lt;th&gt;One-line cause&lt;/th&gt;
&lt;th&gt;One-line fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Single Point of Failure&lt;/td&gt;
&lt;td&gt;No redundancy on a critical-path component&lt;/td&gt;
&lt;td&gt;Redundancy + tested automatic failover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Cascading Failure&lt;/td&gt;
&lt;td&gt;Slow dependency exhausts caller's thread pool, propagates upstream&lt;/td&gt;
&lt;td&gt;Timeouts, circuit breakers, bulkheads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Retry Storm&lt;/td&gt;
&lt;td&gt;Synchronized retries multiply load on a just-recovered service&lt;/td&gt;
&lt;td&gt;Exponential backoff + jitter, retry budget, circuit breaker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Cache Stampede&lt;/td&gt;
&lt;td&gt;Popular key expires, all misses hit DB simultaneously&lt;/td&gt;
&lt;td&gt;Request coalescing, early refresh, jittered TTLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Hot Partition&lt;/td&gt;
&lt;td&gt;Low-cardinality shard key skews load to one shard&lt;/td&gt;
&lt;td&gt;High-cardinality key, salting, per-shard metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Replication Lag&lt;/td&gt;
&lt;td&gt;Replica behind primary, stale read after a write&lt;/td&gt;
&lt;td&gt;Read-your-writes routing, replication-position tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Duplicate Processing&lt;/td&gt;
&lt;td&gt;Retried request re-executes a non-idempotent side effect&lt;/td&gt;
&lt;td&gt;Idempotency keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Queue Backlog&lt;/td&gt;
&lt;td&gt;Producer rate &amp;gt; consumer rate, backlog grows silently&lt;/td&gt;
&lt;td&gt;Lag-based alerting/autoscaling, bounded queue, load shedding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Poison Message&lt;/td&gt;
&lt;td&gt;Malformed message crash-loops the consumer forever&lt;/td&gt;
&lt;td&gt;Capped attempts → Dead Letter Queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Split Brain&lt;/td&gt;
&lt;td&gt;Partition causes two nodes to both act as primary&lt;/td&gt;
&lt;td&gt;Quorum writes, consensus leader election, fencing tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Distributed systems fail in a recurring, nameable set of ways: missing redundancy, failures propagating through tight coupling (timeouts/thread pools), synchronized client behavior (retries, cache expiry), uneven data/traffic distribution, staleness from replication, non-idempotent side effects, and disagreements about leadership during partitions. Each one has a well-understood fix — redundancy, timeouts/circuit breakers/bulkheads, backoff with jitter, request coalescing, better shard keys, staleness-aware read routing, idempotency keys, lag-based backpressure, dead letter queues, and quorum-based consensus. Designing a system well means assuming each of these will eventually happen, and deciding in advance how the system should behave when it does.&lt;/p&gt;

</description>
      <category>techlettersbyteja</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>API Gateway vs Load Balancer vs Reverse Proxy</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Sat, 10 Oct 2026 04:34:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/api-gateway-vs-load-balancer-vs-reverse-proxy-4kkc</link>
      <guid>https://dev.to/tejaswipandava/api-gateway-vs-load-balancer-vs-reverse-proxy-4kkc</guid>
      <description>&lt;p&gt;In system design and distributed architectures, these three components are often confused, but they serve &lt;strong&gt;distinct roles&lt;/strong&gt; — and in practice, a single production system usually uses &lt;strong&gt;all three together&lt;/strong&gt;, layered on top of each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣ API Gateway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Acts as a &lt;strong&gt;single entry point&lt;/strong&gt; for client-facing APIs — crucial in a microservices architecture where dozens of services would otherwise each need their own public endpoint.&lt;/li&gt;
&lt;li&gt;Handles &lt;strong&gt;authentication, authorization, and rate limiting&lt;/strong&gt; to secure and monitor API traffic.&lt;/li&gt;
&lt;li&gt;Implements &lt;strong&gt;request routing&lt;/strong&gt; to the correct microservice, and can &lt;strong&gt;aggregate/compose&lt;/strong&gt; multiple backend responses into a single response (API composition / backend-for-frontend).&lt;/li&gt;
&lt;li&gt;Other common responsibilities: request/response transformation, API versioning, request validation, quota management, analytics/logging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; AWS API Gateway, Kong, Apigee, Zuul.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2️⃣ Load Balancer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distributes incoming traffic evenly&lt;/strong&gt; across multiple backend servers/instances for high availability and horizontal scalability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevents server overload&lt;/strong&gt; and provides fault tolerance by detecting failed instances (health checks) and routing away from them.&lt;/li&gt;
&lt;li&gt;Operates at either:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Layer 4 (Transport — TCP/UDP):&lt;/strong&gt; routes based on IP/port, very fast, protocol-agnostic, no visibility into HTTP content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layer 7 (Application — HTTP/HTTPS):&lt;/strong&gt; can route based on URL path, headers, cookies; enables smarter routing (e.g., &lt;code&gt;/api/v1/*&lt;/code&gt; → service A) but adds a bit more overhead.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; AWS ELB/ALB/NLB, NGINX, HAProxy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3️⃣ Reverse Proxy
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sits in front of one or more backend servers and &lt;strong&gt;forwards client requests&lt;/strong&gt; to them — clients never talk to the backend servers directly.&lt;/li&gt;
&lt;li&gt;Adds &lt;strong&gt;security&lt;/strong&gt; by hiding backend server details/IP addresses (backend is not directly internet-addressable), and can filter malicious traffic.&lt;/li&gt;
&lt;li&gt;Commonly performs &lt;strong&gt;SSL/TLS termination&lt;/strong&gt; (decrypts HTTPS at the proxy so backend servers don't bear that CPU cost) and &lt;strong&gt;caching&lt;/strong&gt; of static/responses to improve performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; Nginx, Apache HTTP Server (mod_proxy), Envoy, Traefik.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎯 Key Distinction — Purpose, Not Just Technology
&lt;/h2&gt;

&lt;p&gt;The confusing part: &lt;strong&gt;the same software (e.g., NGINX) can act as all three&lt;/strong&gt;, depending on configuration. The distinction is about &lt;strong&gt;role/intent&lt;/strong&gt;, not the specific product:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;API Gateway&lt;/th&gt;
&lt;th&gt;Load Balancer&lt;/th&gt;
&lt;th&gt;Reverse Proxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary purpose&lt;/td&gt;
&lt;td&gt;API management (auth, rate limit, routing, composition)&lt;/td&gt;
&lt;td&gt;Distribute traffic across many servers&lt;/td&gt;
&lt;td&gt;Forward/hide backend, terminate SSL, cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layer&lt;/td&gt;
&lt;td&gt;Usually L7, API-aware&lt;/td&gt;
&lt;td&gt;L4 or L7&lt;/td&gt;
&lt;td&gt;Usually L7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client-facing?&lt;/td&gt;
&lt;td&gt;Yes, single entry point for APIs&lt;/td&gt;
&lt;td&gt;Sometimes (public-facing) or internal&lt;/td&gt;
&lt;td&gt;Yes, typically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Knows about business logic/APIs?&lt;/td&gt;
&lt;td&gt;Yes (routes per-endpoint, auth per-API)&lt;/td&gt;
&lt;td&gt;No, just traffic distribution&lt;/td&gt;
&lt;td&gt;Not necessarily&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adds auth/rate limiting?&lt;/td&gt;
&lt;td&gt;Yes, core feature&lt;/td&gt;
&lt;td&gt;No (not its job)&lt;/td&gt;
&lt;td&gt;Not typically (can via plugins)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Example&lt;/td&gt;
&lt;td&gt;AWS API Gateway, Kong&lt;/td&gt;
&lt;td&gt;AWS ELB, NGINX (as LB)&lt;/td&gt;
&lt;td&gt;NGINX (as proxy), Envoy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Mental model:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reverse Proxy&lt;/strong&gt; = "I sit in front of your server(s) and hide them; I may also cache/terminate TLS."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load Balancer&lt;/strong&gt; = "I sit in front of &lt;em&gt;many&lt;/em&gt; servers and decide &lt;em&gt;which one&lt;/em&gt; gets each request."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; = "I sit in front of your &lt;em&gt;APIs&lt;/em&gt;, and I understand and manage &lt;em&gt;API-level&lt;/em&gt; concerns (auth, quotas, routing per endpoint, aggregation)."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A Load Balancer and Reverse Proxy are often the &lt;em&gt;same&lt;/em&gt; deployed component performing overlapping duties (many reverse proxies also load balance). An API Gateway is typically a more specialized, higher-level layer sitting &lt;em&gt;in front of&lt;/em&gt; a load balancer, not a replacement for one.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ How They Typically Fit Together in a Real Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  → CDN (optional, static/cacheable content)
    → API Gateway (auth, rate limiting, routing, composition)
      → Load Balancer (distributes across service instances)
        → Reverse Proxy (per-service, TLS termination/caching) [often merged into the LB layer]
          → Backend service instances
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In many real deployments, the reverse proxy and load balancer are literally the same NGINX/Envoy instance — "reverse proxy" describes &lt;em&gt;what&lt;/em&gt; it's doing (hiding/forwarding to backends), while "load balancer" describes &lt;em&gt;how&lt;/em&gt; it decides where to forward (distribution algorithm).&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Q&amp;amp;A
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can one tool act as all three (API Gateway, LB, and Reverse Proxy)?&lt;/strong&gt;&lt;br&gt;
Yes — NGINX and Envoy are commonly configured to do all three simultaneously: terminate TLS (reverse proxy), distribute traffic across upstream instances (load balancer), and with plugins/extensions (or paired with Kong), handle auth/rate-limiting/routing (API gateway). The distinction is about the &lt;em&gt;role being performed&lt;/em&gt;, not a strict 1:1 mapping to a specific product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why would you need both an API Gateway and a Load Balancer?&lt;/strong&gt;&lt;br&gt;
The API Gateway handles cross-cutting API concerns (auth, quotas, per-endpoint routing, composition) once, at the edge — it typically then forwards the request to a load balancer, which handles the mechanics of picking &lt;em&gt;which specific healthy instance&lt;/em&gt; of that service should serve the request. They operate at different levels of abstraction: API Gateway = "which service, with what policy," Load Balancer = "which instance of that service, right now." This also answers where rate limiting belongs — at the API Gateway, since it's an API-level policy tied to caller identity, not something a Load Balancer (which is only aware of traffic distribution and health) is designed to handle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L4 vs L7 load balancing — when would you choose each?&lt;/strong&gt;&lt;br&gt;
L4 (TCP/UDP) is faster and simpler — good when you just need raw connection distribution and don't care about HTTP semantics (e.g., a database proxy, or extremely high-throughput traffic where every microsecond counts). L7 (HTTP-aware) is needed when routing decisions depend on the request content — path-based routing (&lt;code&gt;/api/*&lt;/code&gt; vs &lt;code&gt;/static/*&lt;/code&gt;), header/cookie-based session affinity, or A/B testing/canary routing by request attributes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does a reverse proxy improve security?&lt;/strong&gt;&lt;br&gt;
It hides backend server IPs/topology from the public internet (backends aren't directly addressable), centralizes where security policies (WAF rules, IP allowlisting, TLS termination/certificate management) are enforced, and can absorb/filter malicious traffic (e.g., basic DDoS mitigation, malformed request rejection) before it ever reaches application servers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between a forward proxy and a reverse proxy?&lt;/strong&gt;&lt;br&gt;
A forward proxy sits in front of &lt;strong&gt;clients&lt;/strong&gt; and makes requests on their behalf to the internet (e.g., corporate proxy hiding internal users from external sites — the &lt;em&gt;server&lt;/em&gt; doesn't know who the real client is). A reverse proxy sits in front of &lt;strong&gt;servers&lt;/strong&gt; and handles requests on their behalf from clients (the &lt;em&gt;client&lt;/em&gt; doesn't know which backend server actually served the request).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does adding a Reverse Proxy/Load Balancer/API Gateway add latency?&lt;/strong&gt;&lt;br&gt;
Technically each hop adds some latency (extra network hop + processing), but it's usually negligible (sub-millisecond to a few ms) compared to the benefits: security, TLS offload, caching, smarter routing, and horizontal scalability — which &lt;em&gt;reduce&lt;/em&gt; overall latency far more than the hop itself costs, especially under load.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;An API Gateway manages API-level concerns like auth, rate limiting, and routing across microservices. A Load Balancer distributes traffic across multiple instances of a service for availability and scale. A Reverse Proxy sits in front of backend servers to hide them, terminate TLS, and cache responses. In practice they're often layered together, and the same software (e.g., NGINX) can perform all three roles depending on configuration.&lt;/p&gt;

</description>
      <category>techlettersbyteja</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Latency vs Throughput vs Bandwidth</title>
      <dc:creator>tejaswipandava</dc:creator>
      <pubDate>Fri, 09 Oct 2026 03:33:00 +0000</pubDate>
      <link>https://dev.to/tejaswipandava/latency-vs-throughput-vs-bandwidth-4mgo</link>
      <guid>https://dev.to/tejaswipandava/latency-vs-throughput-vs-bandwidth-4mgo</guid>
      <description>&lt;p&gt;Many engineers use these terms interchangeably, but they measure three different things. Understanding the distinction helps you design better systems and reason clearly about networking and distributed systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  1️⃣ Latency
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition:&lt;/strong&gt; The time it takes for data to travel from sender to receiver — "how long does a single request take?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API response time&lt;/li&gt;
&lt;li&gt;Database query time&lt;/li&gt;
&lt;li&gt;Network round-trip time (RTT)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a request takes 40 ms to reach its destination, the latency is 40 ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Units:&lt;/strong&gt; ms, μs&lt;br&gt;
&lt;strong&gt;Lower is better.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2️⃣ Throughput
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition:&lt;/strong&gt; The amount of data/work successfully delivered per unit time — "how much work gets done every second?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests per second (RPS)&lt;/li&gt;
&lt;li&gt;Messages processed per second&lt;/li&gt;
&lt;li&gt;MB transferred per second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A system processing 10,000 requests per second has high throughput.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Units:&lt;/strong&gt; RPS, Mbps, Gbps, Transactions/sec&lt;br&gt;
&lt;strong&gt;Higher is better.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3️⃣ Bandwidth
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Definition:&lt;/strong&gt; The maximum capacity of a network link — "the width of the highway."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 Mbps internet connection&lt;/li&gt;
&lt;li&gt;1 Gbps network interface&lt;/li&gt;
&lt;li&gt;10 Gbps data center link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bandwidth = theoretical maximum. Throughput = what you actually achieve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Units:&lt;/strong&gt; Mbps, Gbps&lt;br&gt;
&lt;strong&gt;Higher is better.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Highway Analogy
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Highway Equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;How long it takes &lt;strong&gt;one car&lt;/strong&gt; to travel from A to B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput&lt;/td&gt;
&lt;td&gt;How many &lt;strong&gt;cars reach the destination per hour&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bandwidth&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Number of lanes&lt;/strong&gt; available on the highway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A highway can have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ High bandwidth, ❌ High latency (many lanes, but the road is very long / has tolls)&lt;/li&gt;
&lt;li&gt;✅ Low latency, ❌ Low throughput (short road, but only 1 lane — a car gets there fast, but few cars fit)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics are related but &lt;strong&gt;not identical&lt;/strong&gt; — you can have low latency with low throughput, or the reverse, and designing for one doesn't automatically improve the other.&lt;/p&gt;




&lt;h2&gt;
  
  
  ❓ Q&amp;amp;A
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How are latency and throughput related, and can a system have one without the other?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Latency is the time for a single unit of work to complete; throughput is the rate at which many units of work complete over time. A system can have low latency but low throughput (a single request is fast, but the system can't handle many concurrently), or higher latency but high throughput (each request is slower, but thousands are processed in parallel via pipelining/batching).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a network have high bandwidth but low throughput?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Bandwidth is the theoretical max capacity; throughput is what's actually achieved. Common causes: protocol overhead, packet loss/retransmission, congestion, small TCP window sizes, server-side processing bottlenecks, inefficient application code, or too few concurrent connections to saturate the link.&lt;/p&gt;

&lt;p&gt;For example, a 100 Mbps link achieving only 60 Mbps throughput is usually explained by real-world losses — TCP/IP header overhead, retransmissions from packet loss, congestion control backing off, the latency/RTT limiting how much data can be "in flight" (bandwidth-delay product), competing traffic sharing the link, or hardware/software processing limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What factors contribute to network latency, and how do you reduce it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Latency is made up of several components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Propagation delay (physical distance, speed of light in fiber)&lt;/li&gt;
&lt;li&gt;Transmission delay (time to push bits onto the wire, depends on bandwidth)&lt;/li&gt;
&lt;li&gt;Processing delay (routers, servers, load balancers, serialization/deserialization)&lt;/li&gt;
&lt;li&gt;Queuing delay (congestion, buffering at routers/servers)&lt;/li&gt;
&lt;li&gt;DNS resolution, TLS handshake, connection setup (TCP 3-way handshake)&lt;/li&gt;
&lt;li&gt;Application-level delays (DB queries, GC pauses, lock contention)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common ways to reduce it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache close to the user (CDN, in-memory, edge caching)&lt;/li&gt;
&lt;li&gt;Reduce network hops / use geo-distributed regions closer to users&lt;/li&gt;
&lt;li&gt;Use connection pooling &amp;amp; keep-alive to avoid repeated handshakes&lt;/li&gt;
&lt;li&gt;Use async/non-blocking I/O to avoid queuing delays&lt;/li&gt;
&lt;li&gt;Batch or pipeline requests where possible&lt;/li&gt;
&lt;li&gt;Optimize serialization (protobuf/avro vs JSON)&lt;/li&gt;
&lt;li&gt;Use faster transport protocols (HTTP/2, QUIC/HTTP/3, gRPC)&lt;/li&gt;
&lt;li&gt;Reduce database query time (indexes, denormalization, read replicas)&lt;/li&gt;
&lt;li&gt;Load balance to avoid hotspots/queuing on a single node&lt;/li&gt;
&lt;li&gt;Precompute / prefetch data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How does caching impact latency?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Caching reduces latency by serving data from a faster, closer store (memory, CDN edge, local cache) instead of round-tripping to a database or origin server. It removes the need for expensive computation/disk/network I/O on repeated requests. It primarily helps &lt;strong&gt;latency&lt;/strong&gt;, and indirectly throughput too, since the backend capacity freed up can serve more requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does horizontal scaling impact throughput?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Adding more instances/nodes behind a load balancer lets a system handle more concurrent requests, directly increasing throughput (more RPS). It typically does &lt;strong&gt;not&lt;/strong&gt; improve — and may slightly worsen — the latency of an individual request (still bounded by network RTT + per-request processing time), unless it also reduces contention/queueing delay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why doesn't increasing bandwidth always reduce latency?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bandwidth affects how much data can be sent per second, not how fast a single bit/packet travels. Latency is dominated by propagation delay (distance/speed of light) and processing/queuing delays — a wider highway (more lanes) doesn't make a single car arrive faster if the road length and speed limit stay the same. Bandwidth helps throughput; it only marginally helps latency by reducing transmission delay for large payloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does latency or throughput/bandwidth matter more for a given use case?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on the workload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Online gaming:&lt;/strong&gt; latency matters most. Games need near real-time responsiveness (low RTT) for actions to feel instant; packet sizes are small, so throughput is rarely the bottleneck. High latency causes lag even with plenty of bandwidth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Video streaming:&lt;/strong&gt; bandwidth and sustained throughput matter most. Streaming needs enough sustained data rate to deliver frames without buffering; a few hundred ms of extra latency (buffering) is imperceptible, but insufficient bandwidth causes stuttering and quality drops. (Exception: &lt;em&gt;live, interactive&lt;/em&gt; video — e.g., video calls — behaves more like gaming and is latency-sensitive.)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💡 Summary
&lt;/h2&gt;

&lt;p&gt;Latency measures how long a request takes, throughput measures how much work is completed per unit time, and bandwidth measures the maximum capacity of the communication channel. Treating these as the same thing is one of the most common mistakes in reasoning about system performance.&lt;/p&gt;

</description>
      <category>techlettersbyteja</category>
      <category>systemdesign</category>
      <category>interview</category>
    </item>
  </channel>
</rss>
