<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Systems at Scale]]></title><description><![CDATA[Systems at Scale]]></description><link>https://systemsatscale.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Systems at Scale</title><link>https://systemsatscale.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 19 Sep 2026 11:35:14 GMT</lastBuildDate><atom:link href="https://systemsatscale.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Tomcat vs Netty: The Thread Model Decision Most Engineers Get Backwards]]></title><description><![CDATA[(Part 1 of 2 - Backend I/O Internals Series)
Every Spring Boot service you've ever written sits on top of a decision you probably never made consciously: does your server hand each request its own thr]]></description><link>https://systemsatscale.hashnode.dev/tomcat-vs-netty-the-thread-model-decision-most-engineers-get-backwards</link><guid isPermaLink="true">https://systemsatscale.hashnode.dev/tomcat-vs-netty-the-thread-model-decision-most-engineers-get-backwards</guid><category><![CDATA[Java]]></category><category><![CDATA[spring-boot]]></category><category><![CDATA[netty]]></category><category><![CDATA[Micronaut]]></category><category><![CDATA[Reactive Programming]]></category><category><![CDATA[Backend Development]]></category><category><![CDATA[Tomcat]]></category><dc:creator><![CDATA[ayushsharmaio0112]]></dc:creator><pubDate>Mon, 14 Sep 2026 20:16:21 GMT</pubDate><content:encoded><![CDATA[<p><em>(Part 1 of 2 - Backend I/O Internals Series)</em></p>
<p>Every Spring Boot service you've ever written sits on top of a decision you probably never made consciously: does your server hand each request its own thread, or does it multiplex thousands of requests over a handful of threads?</p>
<p>Get this wrong for your workload, and you'll see it under load - thread pool exhaustion in one model, event-loop stalls in the other. Get it right, and you'll barely think about it again.</p>
<p>This post builds the mental model from the ground up: what blocking I/O actually costs you at the OS level, how Java's NIO makes non-blocking I/O possible, and how that plays out differently in Tomcat's thread-per-request design versus Netty's event loop. <strong>Part 2</strong> takes this model and applies it to Spring MVC, WebFlux, and Micronaut - including the single most common mistake teams make when they switch to a "non-blocking" stack.</p>
<p><strong>Table of contents</strong></p>
<ol>
<li><p><a href="#blocking-vs-non-blocking-io">Blocking vs Non-blocking I/O</a></p>
</li>
<li><p><a href="#java-nio--selector">Java NIO &amp; Selector</a></p>
</li>
<li><p><a href="#tomcat-architecture">Tomcat architecture</a></p>
</li>
<li><p><a href="#netty-architecture">Netty architecture</a></p>
</li>
<li><p><a href="#tomcat-vs-netty-thread-model">Tomcat vs Netty thread model</a></p>
</li>
</ol>
<hr />
<h2>Blocking vs Non-blocking I/O</h2>
<p>At the OS level, I/O - reading a socket, querying a database, calling another service - means asking the kernel to do something and waiting for it to finish.</p>
<p><strong>Blocking I/O</strong> means the calling thread parks itself until that operation completes. It can't do anything else. If you have one thread per request and that request is waiting on a slow downstream call, that thread is dead weight for the duration - allocated, scheduled, consuming stack memory, and doing absolutely nothing useful.</p>
<p><strong>Non-blocking I/O</strong> means the thread issues the request and immediately moves on. It doesn't wait. When the operation completes, something (an event loop, a callback, a selector) notifies the system, and the result gets picked up later - often by a <em>different</em> thread, or the same thread on its next pass through a loop.</p>
<p>Think of it like a restaurant. A blocking model is a waiter who takes one table's order, stands at the kitchen window until the food is ready, delivers it, and only then moves to the next table. A non-blocking model is a waiter who takes an order, moves on to the next table immediately, and gets pinged when any order is ready to deliver - one waiter, many tables, no idle standing around.</p>
<p>This single distinction - does the thread wait, or does it move on - is the root of nearly every architectural difference between Tomcat and Netty, and between Spring MVC and WebFlux. Everything else in this post is a consequence of that choice.</p>
<h2>Java NIO &amp; Selector</h2>
<p>Java's original I/O (<code>java.io</code>) is blocking by default - a <code>InputStream.read()</code> call parks the thread. Java NIO (<code>java.nio</code>), introduced to support non-blocking I/O, is built on three core pieces:</p>
<ul>
<li><p><strong>Channels</strong>: bidirectional conduits for data (a <code>SocketChannel</code>, for instance), replacing the one-directional streams of <code>java.io</code></p>
</li>
<li><p><strong>Buffers</strong>: fixed-size containers you read from and write into, since NIO deals in chunks rather than continuous streams</p>
</li>
<li><p><strong>Selector</strong>: the piece that makes non-blocking I/O actually useful. A <code>Selector</code> lets a single thread monitor many channels at once, and only wakes up when one of them is actually ready (readable, writable, or has a new connection)</p>
</li>
</ul>
<p>The Selector is the multiplexing mechanism: instead of one thread per connection, you get one thread watching thousands of connections, springing into action only when there's real work to do. This is the low-level primitive that Netty and every non-blocking server is built directly on top of. You rarely touch <code>Selector</code> yourself in application code, but understanding that it exists explains <em>why</em> an event loop can handle so many connections with so few threads.</p>
<h2>Tomcat architecture</h2>
<p>Tomcat, the default embedded server in Spring Boot, uses a <strong>thread-per-request</strong> model (technically thread-per-connection, pooled). When a request comes in:</p>
<ol>
<li><p>A thread is pulled from Tomcat's worker thread pool</p>
</li>
<li><p>That thread handles the entire request lifecycle- reading input, running your application code, writing the response</p>
</li>
<li><p>If your code makes a blocking call (a DB query, an HTTP call to another service), the thread sits there, blocked, until it returns</p>
</li>
<li><p>Only then is the thread returned to the pool for the next request</p>
</li>
</ol>
<p>The scaling lever here is <strong>thread pool size</strong>. Increase it, and you can handle more concurrent in-flight requests up to a point. Each thread costs memory (default JVM thread stacks are ~1MB), and past a few hundred to a couple thousand threads, context-switching overhead and memory pressure start working against you. Under a traffic spike where downstream calls slow down, every thread ends up blocked waiting, the pool exhausts, and new requests queue or get rejected even though the CPU itself may be nearly idle.</p>
<p>This model is simple to reason about one thread, one request, top to bottom - which is exactly why it's been the default for so long.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8a0b9a1437e67b094ea235/1d8b4e64-5e4b-492b-bb61-3f209e91192b.png" alt="Tomcat thread-per-request model" style="display:block;margin:0 auto" />

<h2>Netty architecture</h2>
<p>Netty takes the opposite approach: a small, fixed number of threads (typically close to CPU core count) organized into an <strong>event loop group</strong>, each running an event loop built on NIO's Selector.</p>
<p>Each event loop thread can handle thousands of connections simultaneously, because it's never blocked waiting on any single one. It processes whatever's ready- a readable socket here, a writable one there - cycles through, and moves on. A "request" doesn't own a thread for its lifetime; it occupies a thread only for the brief moments it has actual CPU work to do.</p>
<p>This is why Netty-based servers can sustain enormous numbers of concurrent connections with a handful of threads, where Tomcat would need a thread per in-flight request. The tradeoff: your code has to cooperate. If anything you run inside an event loop blocks, it doesn't just stall one request - it stalls every connection multiplexed on that thread. <strong>That failure mode is the entire subject of Part 2.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a8a0b9a1437e67b094ea235/7e352cd3-7990-4516-a225-ed87fd1162fe.png" alt="Netty event loop model" style="display:block;margin:0 auto" />

<h2>Tomcat vs Netty thread model</h2>
<table>
<thead>
<tr>
<th></th>
<th>Tomcat (thread-per-request)</th>
<th>Netty (event loop)</th>
</tr>
</thead>
<tbody><tr>
<td>Threads under load</td>
<td>Grows with concurrent requests (bounded by pool size)</td>
<td>Stays flat - fixed small number</td>
</tr>
<tr>
<td>Blocked thread cost</td>
<td>One request stalled, one thread wasted</td>
<td>One blocked call can stall <em>every</em> request on that event loop thread</td>
</tr>
<tr>
<td>Memory footprint at scale</td>
<td>High (thread stacks scale with concurrency)</td>
<td>Low (few threads regardless of connection count)</td>
</tr>
<tr>
<td>Failure mode under spike</td>
<td>Thread pool exhaustion, request queuing/rejection</td>
<td>Event loop starvation if blocking code leaks in</td>
</tr>
<tr>
<td>Mental model</td>
<td>Simple - linear, one thread per request</td>
<td>Requires discipline - must stay non-blocking end-to-end</td>
</tr>
</tbody></table>
<p>The core insight: Tomcat trades memory/thread overhead for simplicity and fault isolation (one slow request doesn't take down others). Netty trades that isolation for massive connection scalability - but only if you hold up your end of the bargain and never block an event loop thread.</p>
<hr />
<h3>Coming up in Part 2</h3>
<p>You now know <em>why</em> Tomcat and Netty behave differently under load. But the frameworks built on top of them - Spring MVC, WebFlux, Micronaut are where this decision actually gets made in practice, and where one very common mistake quietly undoes the entire point of switching to a non-blocking stack.</p>
<p><strong>Part 2 covers:</strong></p>
<ul>
<li><p>Spring MVC's request lifecycle (and why blocking calls are completely natural there)</p>
</li>
<li><p>Spring WebFlux + Netty and the one condition that has to hold for it to actually pay off</p>
</li>
<li><p>Micronaut's compile-time DI what it buys you over Spring's runtime reflection</p>
</li>
<li><p><strong>The blocking-call trap</strong>: how a single synchronous JDBC call inside a reactive handler can silently stall dozens of unrelated requests</p>
</li>
<li><p>A decision framework for when each stack is actually worth it</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Stop Confusing API Gateway, Load Balancer, and Reverse Proxy]]></title><description><![CDATA[If you've ever been in a system design interview or an architecture review and heard someone say "just put a load balancer in front of it" when they actually meant an API Gateway - you're not alone. T]]></description><link>https://systemsatscale.hashnode.dev/stop-confusing-api-gateway-load-balancer-and-reverse-proxy</link><guid isPermaLink="true">https://systemsatscale.hashnode.dev/stop-confusing-api-gateway-load-balancer-and-reverse-proxy</guid><dc:creator><![CDATA[ayushsharmaio0112]]></dc:creator><pubDate>Sat, 22 Aug 2026 21:09:52 GMT</pubDate><content:encoded><![CDATA[<p>If you've ever been in a system design interview or an architecture review and heard someone say "just put a load balancer in front of it" when they actually meant an API Gateway - you're not alone. These three components sit in similar places in a request's journey, and they even share some overlapping capabilities. That overlap is exactly why people mix them up.</p>
<p>This post breaks down what each one actually does, where it sits in your architecture, and - most importantly - when to use which.</p>
<h2>The Core Problem: They All "Sit in Front" of Something</h2>
<p>At a glance, all three:</p>
<ul>
<li><p>Receive incoming traffic</p>
</li>
<li><p>Sit between the client and your backend services</p>
</li>
<li><p>Can do TLS termination</p>
</li>
<li><p>Can route requests somewhere</p>
</li>
</ul>
<p>Because of this surface-level similarity, engineers often treat them as interchangeable. They're not. Each was built to solve a different problem, and understanding <em>why</em> each one exists is the key to telling them apart.</p>
<hr />
<h2>Reverse Proxy: The Traffic Doorman</h2>
<p>A <strong>reverse proxy</strong> sits in front of one or more backend servers and forwards client requests to them. The client only ever talks to the proxy - it has no idea what's happening behind it.</p>
<p><strong>Primary job:</strong> Hide backend infrastructure, forward requests, and optionally cache or compress responses.</p>
<p><strong>Typical responsibilities:</strong></p>
<ul>
<li><p>TLS termination (decrypting HTTPS so backend servers don't have to)</p>
</li>
<li><p>Basic request routing (e.g., <code>/images</code> → image server, <code>/api</code> → app server)</p>
</li>
<li><p>Caching static content</p>
</li>
<li><p>Compression (gzip/brotli)</p>
</li>
<li><p>Hiding internal server details and IP addresses</p>
</li>
<li><p>Basic protection (rate limiting, blocking bad IPs)</p>
</li>
</ul>
<p><strong>Common tools:</strong> Nginx, HAProxy (in proxy mode), Apache HTTP Server, Envoy</p>
<p><strong>Mental model:</strong> Think of a hotel concierge. Guests (clients) never directly call a specific room — they talk to the concierge, who quietly figures out where to send the request.</p>
<hr />
<h2>Load Balancer: The Traffic Distributor</h2>
<p>A <strong>load balancer</strong> is a specialized type of reverse proxy whose main job is distributing incoming traffic across <em>multiple identical</em> backend instances to prevent any single server from being overwhelmed.</p>
<p><strong>Primary job:</strong> Even out traffic across a pool of servers for scalability and fault tolerance.</p>
<p><strong>Typical responsibilities:</strong></p>
<ul>
<li><p>Distributing requests using algorithms like round-robin, least connections, or weighted routing</p>
</li>
<li><p>Health checks — detecting and routing around unhealthy instances</p>
</li>
<li><p>Failover when a server goes down</p>
</li>
<li><p>Operating at Layer 4 (TCP/UDP) or Layer 7 (HTTP)</p>
</li>
</ul>
<p><strong>Common tools:</strong> AWS ELB/ALB/NLB, Nginx, HAProxy, Google Cloud Load Balancer, F5</p>
<p><strong>Mental model:</strong> Think of an airport check-in area with multiple counters and a staff member directing you: "Counter 4 is free, go there." The staff member doesn't care what's <em>in</em> your request - they just care that no single counter gets overloaded.</p>
<p><strong>Key distinction from Reverse Proxy:</strong> Every load balancer is technically a reverse proxy, but not every reverse proxy is a load balancer. A reverse proxy might forward all traffic to a single server; a load balancer's entire purpose is spreading traffic across <em>many</em>.</p>
<hr />
<h2>API Gateway: The Traffic Manager with Business Logic</h2>
<p>An <strong>API Gateway</strong> is a reverse proxy on steroids, purpose-built for managing APIs - particularly in microservices architectures. It doesn't just forward requests; it understands and acts on the <em>content</em> of the API request itself.</p>
<p><strong>Primary job:</strong> Be the single entry point for clients to interact with potentially dozens of backend microservices, while handling cross-cutting API concerns.</p>
<p><strong>Typical responsibilities:</strong></p>
<ul>
<li><p>Authentication and authorization (validating API keys, JWTs, OAuth tokens)</p>
</li>
<li><p>Rate limiting and throttling per client/API key</p>
</li>
<li><p>Request/response transformation (e.g., XML to JSON)</p>
</li>
<li><p>Routing requests to the correct microservice based on path, headers, or version</p>
</li>
<li><p>API versioning</p>
</li>
<li><p>Aggregating responses from multiple services into one</p>
</li>
<li><p>Analytics, logging, and monitoring of API usage</p>
</li>
<li><p>Sometimes load balancing <em>too</em> - as an added feature</p>
</li>
</ul>
<p><strong>Common tools:</strong> Kong, AWS API Gateway, Apigee, Zuul, Amazon Kong, Tyk, Traefik (partially)</p>
<p><strong>Mental model:</strong> Think of a corporate receptionist who doesn't just point you to a room - they check your ID, verify you have an appointment, log your visit, decide which department actually handles your request, and sometimes even collect information from three departments before giving you a single combined answer.</p>
<hr />
<h2>Side-by-Side Comparison</h2>
<table>
<thead>
<tr>
<th>Aspect</th>
<th>Reverse Proxy</th>
<th>Load Balancer</th>
<th>API Gateway</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Main goal</strong></td>
<td>Hide &amp; forward requests</td>
<td>Distribute traffic across servers</td>
<td>Manage API traffic &amp; policies</td>
</tr>
<tr>
<td><strong>Awareness of request content</strong></td>
<td>Minimal</td>
<td>Minimal (mostly connection-level)</td>
<td>Deep (understands API semantics)</td>
</tr>
<tr>
<td><strong>Auth/authorization</strong></td>
<td>Rarely</td>
<td>No</td>
<td>Yes - core feature</td>
</tr>
<tr>
<td><strong>Rate limiting per client</strong></td>
<td>Basic, if any</td>
<td>No</td>
<td>Yes - core feature</td>
</tr>
<tr>
<td><strong>Health checks &amp; failover</strong></td>
<td>Sometimes</td>
<td>Yes - core feature</td>
<td>Sometimes (delegated)</td>
</tr>
<tr>
<td><strong>Request/response transformation</strong></td>
<td>No</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td><strong>Typical layer</strong></td>
<td>L7 (sometimes L4)</td>
<td>L4 or L7</td>
<td>L7 only</td>
</tr>
<tr>
<td><strong>Best suited for</strong></td>
<td>General traffic hiding/routing</td>
<td>Scaling horizontally across servers</td>
<td>Microservices, external API exposure</td>
</tr>
</tbody></table>
<hr />
<h2>Where They Actually Overlap (Why the Confusion Exists)</h2>
<ul>
<li><p>A load balancer <strong>is</strong> a reverse proxy - just with a narrower, traffic-distribution-focused purpose.</p>
</li>
<li><p>An API Gateway <strong>often includes</strong> reverse proxy and load balancing capabilities.</p>
</li>
<li><p>Tools like <strong>Nginx</strong>, <strong>Envoy</strong>, and <strong>Traefik</strong> can be configured to act as any of the three, which blurs the lines in practice.</p>
</li>
</ul>
<p>This is the real source of confusion: it's not that these are unrelated concepts, it's that modern tools are flexible enough to <em>wear multiple hats</em>. The distinction lives in <strong>intent and primary responsibility</strong>, not necessarily in the tool itself.</p>
<h2>A Simplified Way to Remember It</h2>
<ul>
<li><p><strong>Reverse Proxy</strong> → "I forward your request somewhere and hide what's back there."</p>
</li>
<li><p><strong>Load Balancer</strong> → "I make sure no single server gets crushed by traffic."</p>
</li>
<li><p><strong>API Gateway</strong> → "I understand your API request, check who you are, and decide what happens to it."</p>
</li>
</ul>
<h2>A Real-World Architecture Example</h2>
<p>Imagine a typical microservices-based e-commerce app:</p>
<ol>
<li><p><strong>Client</strong> sends a request to <code>api.mystore.com/orders</code></p>
</li>
<li><p><strong>API Gateway</strong> authenticates the request, checks rate limits, and routes it to the <code>orders</code> microservice</p>
</li>
<li><p>Behind the gateway, a <strong>Load Balancer</strong> distributes that request across 5 instances of the <code>orders</code> service running for redundancy</p>
</li>
<li><p>Each instance might sit behind a <strong>Reverse Proxy</strong> (like Nginx) that terminates TLS and forwards to the actual application process</p>
</li>
</ol>
<p>All three components can coexist in the same request path, each solving a different problem at a different layer.</p>
<h2>Final Takeaway</h2>
<p>These three aren't competing technologies - they're complementary layers that often work together. The confusion comes from overlapping capabilities in modern tools, not from the concepts themselves being similar. Once you anchor on <strong>primary intent</strong> - hiding backend, distributing load, or managing API policy - the fog clears fast.</p>
<p>Next time someone says "just load balance it" when they mean "add an API gateway," you'll know exactly why that matters.</p>
]]></content:encoded></item></channel></rss>