<?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[System Design 101]]></title><description><![CDATA[System Design 101]]></description><link>https://nithizh.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 14 Nov 2026 18:58:57 GMT</lastBuildDate><atom:link href="https://nithizh.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why We Replaced Kafka with NATS — And Never Looked Back]]></title><description><![CDATA[How a SaaS platform solved the enterprise firewall problem and why NATS turned out to be a better fit than Kafka for async microservice communication.]]></description><link>https://nithizh.hashnode.dev/why-we-replaced-kafka-with-nats-and-never-looked-back</link><guid isPermaLink="true">https://nithizh.hashnode.dev/why-we-replaced-kafka-with-nats-and-never-looked-back</guid><category><![CDATA[nats]]></category><category><![CDATA[kafka]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Microservices]]></category><category><![CDATA[distributed system]]></category><category><![CDATA[#backed]]></category><category><![CDATA[engineering]]></category><dc:creator><![CDATA[Nithish Anand B]]></dc:creator><pubDate>Fri, 13 Nov 2026 16:34:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/9b1dce75-bd7b-4002-8f9e-f4ee44afc258.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>The Problem Nobody Talks About in SaaS</h2>
<p>You've built a great SaaS product. Your backend is a clean set of microservices. You're using Kafka for async communication between them. Life is good.</p>
<p>Then a large enterprise customer signs up.</p>
<p>They want your platform to connect to their internal systems — a CRM, an ERP, a data warehouse sitting inside their private corporate network. You say sure, we'll just call your API. They say... "our servers are not publicly accessible."</p>
<p>You ask them to open a firewall port. Their security team says no.</p>
<p>You suggest a VPN. Their IT team says that'll take 3 months to approve.</p>
<p>And just like that, your SaaS product has hit an invisible wall that no amount of clever code can break through.</p>
<p>This is a problem that thousands of SaaS companies run into — and very few have a clean answer for. This article is about how <strong>NATS</strong> gave us that clean answer.</p>
<hr />
<h2>First, Let's Talk About How We Got Here</h2>
<h3>The Microservices Promise</h3>
<p>When we moved from a monolith to microservices, we immediately needed a way for services to talk to each other without being tightly coupled. Service A shouldn't have to know that Service B even exists, let alone its URL.</p>
<p>That's why message brokers became standard. Instead of calling each other directly, services publish events to a central broker. Other services subscribe and react. Everyone is decoupled. It's beautiful — in theory.</p>
<h3>Why We Chose Kafka</h3>
<p>Kafka is the most popular choice, and for good reason. It's battle-tested, it handles massive volumes of data, and it has a rich ecosystem of tools. For internal service communication — Order Service publishing events that Payment Service consumes — it works well.</p>
<p>But here's what we slowly discovered: Kafka is built for a very specific job — <strong>high-throughput data streaming inside a controlled network</strong>. It assumes everything is on the same network, or at least that you control both ends.</p>
<p>When reality doesn't match that assumption, things get painful.</p>
<hr />
<h2>The Moment Kafka Wasn't Enough</h2>
<h3>Scenario: Syncing with Customer's Private Server</h3>
<p>Imagine your SaaS platform needs to push processed data back to a customer's on-premise server in real time. Maybe it's a healthcare company syncing records to their local database. Maybe it's a manufacturing plant syncing events from your platform to their floor systems.</p>
<p>Here's what your first instinct looks like:</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/db77e8f7-787e-416a-b1f3-0a6e3d48edc6.png" alt="mermaid-diagram-2026-11-13-215334" /></p>
<p>Simple. Except it doesn't work. Because the customer's server is behind a corporate firewall, and no inbound port is open to the outside world. Your HTTP request hits a wall.</p>
<p>Your second instinct:</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/a71bb4fe-af12-4c8b-9706-c74befea9206.png" alt="mermaid-diagram-2026-11-14-085214" /></p>
<p>Still doesn't work. Kafka requires the broker to be reachable. If you run the broker in your cloud, the customer's on-premise consumer needs to reach it on specific ports — which the firewall blocks. If you try to run a Kafka consumer inside their network, it needs to connect out to your broker, which is possible, but Kafka wasn't designed for this topology. It becomes a networking project.</p>
<h3>The Real Root Cause</h3>
<p>The fundamental issue is <strong>who opens the connection</strong>.</p>
<p>Traditional networking assumes the <em>client</em> (the one who wants data) calls the <em>server</em> (the one who has data). Firewalls are designed around this — they block inbound connections from the outside, but allow outbound connections from the inside.</p>
<p>A corporate firewall lets employees browse the internet (outbound). It blocks hackers from reaching internal servers (inbound). That's the whole point.</p>
<p>So if <em>your</em> SaaS is trying to push data <em>into</em> the customer's network, you're going inbound — and the firewall stops you every time.</p>
<p>What we needed was a system where the <strong>customer's side initiates the connection outbound</strong> — and then communication flows both ways through that single outbound tunnel.</p>
<p>That's exactly what NATS does.</p>
<hr />
<h2>Enter NATS</h2>
<h3>What Is NATS?</h3>
<p>NATS is a messaging system — like Kafka, but designed from the ground up for a different world. While Kafka is a freight train optimized for carrying massive loads along a fixed track, NATS is more like a smart postal network that routes messages anywhere, even across network boundaries that shouldn't logically be crossable.</p>
<p>The NATS server is a single tiny binary — about 20 MB. No Java, no JVM, no ZooKeeper, no Kafka Connect. It starts in under a second. The entire thing uses less memory than Kafka's idle footprint.</p>
<table>
<thead>
<tr>
<th></th>
<th>Kafka</th>
<th>NATS</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Memory (idle)</strong></td>
<td>~500 MB – 1 GB</td>
<td>~10–20 MB</td>
</tr>
<tr>
<td><strong>Dependencies</strong></td>
<td>ZooKeeper / KRaft, JVM</td>
<td>None — single binary</td>
</tr>
<tr>
<td><strong>Startup time</strong></td>
<td>10–30 seconds</td>
<td>Under 1 second</td>
</tr>
<tr>
<td><strong>Protocol</strong></td>
<td>Binary, complex</td>
<td>Simple text protocol</td>
</tr>
<tr>
<td><strong>Built for</strong></td>
<td>High-throughput streaming</td>
<td>Low-latency, distributed systems</td>
</tr>
<tr>
<td><strong>Edge / private network support</strong></td>
<td>❌ Not designed for it</td>
<td>✅ First-class feature</td>
</tr>
</tbody></table>
<p>For pure internal streaming at Petabyte scale, Kafka wins. For everything else — especially when you need to cross network boundaries — NATS is a far better fit.</p>
<hr />
<h2>How NATS Solves the Firewall Problem</h2>
<h3>The Concept: Outbound-Only Connection</h3>
<p>NATS has a feature called <strong>Leaf Nodes</strong>. Here's the idea in plain English:</p>
<p>You run a small NATS server inside the customer's private network. This small server reaches <em>out</em> to your cloud NATS hub — just like a browser reaching out to a website. It's an outbound connection, so the firewall allows it.</p>
<p>Once that connection is established, messages flow both ways through it. Your cloud services can send data to the customer's systems. The customer's systems can send data back to you. All through that single outbound tunnel that the customer's server opened.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/bb734c18-3be4-4fe6-9a3a-3fc38aaa2762.png" alt="mermaid-diagram-2026-11-14-085346" /></p>
<p><strong>The customer never opens a single inbound firewall port.</strong> Their security team is happy. You get real-time, two-way communication with their private systems.</p>
<h3>What Happens When the Connection Drops?</h3>
<p>This is where it gets really elegant. If the customer's internet goes down or the WAN link becomes unstable, the small NATS server on their side doesn't crash — it just keeps serving local apps. When connectivity is restored, it reconnects to your cloud hub and picks up where it left off.</p>
<p>This is called <strong>local autonomy</strong>. The customer's systems keep working even when the cloud is unreachable.</p>
<hr />
<h2>Understanding NATS Features (The Easy Version)</h2>
<p>Now that you understand <em>why</em> NATS exists, let's look at what it actually does. NATS has a few core features that together cover almost everything a distributed system needs.</p>
<h3>1. Publish / Subscribe — The Foundation</h3>
<p>This is the basic building block. A service publishes a message to a <strong>subject</strong> (think of it like a topic or channel name). Any service that has subscribed to that subject receives the message.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/460521fb-1e66-462a-bd6c-64c75087369b.png" alt="mermaid-diagram-2026-11-14-085426" /></p>
<p>The publisher has no idea who is listening. It just puts the message out there. This is what makes services decoupled.</p>
<p>Subjects in NATS are hierarchical with dot notation, and you can use wildcards:</p>
<ul>
<li><code>orders.created</code> — exact match</li>
<li><code>orders.*</code> — any single word (orders.created, orders.cancelled, etc.)</li>
<li><code>orders.&gt;</code> — anything starting with "orders."</li>
</ul>
<p>This lets a monitoring service subscribe to <code>&gt;</code> and see every message on the entire system.</p>
<h3>2. Request / Reply — Async RPC</h3>
<p>Sometimes a service needs an answer, not just a one-way notification. NATS has this built in. One service asks a question, another answers.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/37cf986d-15ed-45b3-952f-ed640b5b3cc6.png" alt="mermaid-diagram-2026-11-14-085454" /></p>
<p>This happens over the same NATS connection. No separate HTTP server needed. No REST API to maintain. Services speak to each other directly through the message bus.</p>
<h3>3. Queue Groups — Load Balancing for Free</h3>
<p>If you have 5 instances of your Payment Service all subscribed to the same subject, normally all 5 would receive every message. That's usually not what you want.</p>
<p>With a <strong>queue group</strong>, NATS automatically load-balances — only <em>one</em> instance gets each message. Scale up by adding more instances. Scale down by removing them. No configuration changes, no rebalancing time. NATS handles it instantly.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/3931e76f-e8f7-4840-a8b9-699fe9ca24f5.png" alt="mermaid-diagram-2026-11-14-085527" /></p>
<h3>4. JetStream — When You Need Persistence</h3>
<p>The features above are real-time only. If a service is down when a message is published, it misses that message. For events that must never be lost — payments, orders, audit logs — you need <strong>JetStream</strong>.</p>
<p>JetStream is NATS' built-in persistence layer. Think of it as giving NATS a memory.</p>
<p>You define a <strong>Stream</strong> that captures messages on certain subjects and stores them on disk. Then you define <strong>Consumers</strong> — services that read from that stream at their own pace.</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/f694898e-f5dc-49d7-a004-2c05947d4288.png" alt="mermaid-diagram-2026-11-14-085600" /></p>
<p>The key benefits:</p>
<ul>
<li><strong>At-least-once delivery</strong> — if a service crashes mid-processing, the message is re-delivered</li>
<li><strong>Replay</strong> — a new service can replay all historical events from day one</li>
<li><strong>Consumer lag tracking</strong> — you can see exactly how far behind each service is in real time</li>
</ul>
<blockquote>
<p>Think of a Stream as a DVR recording. Even if you weren't home when the show aired, you can watch it later. The Consumer is your remote control — you can pause, rewind, or skip ahead.</p>
</blockquote>
<h3>5. Key-Value Store — Live Configuration</h3>
<p>NATS has a built-in distributed key-value store, backed by JetStream. This sounds simple but it's incredibly powerful in practice.</p>
<p>You can store configuration values in NATS KV, and all your services watch for changes in real time. When you update a feature flag, every running instance of every service sees the new value within milliseconds — no restart required.</p>
<p>You also get full revision history. Want to know who changed what and when? It's all there.</p>
<blockquote>
<p>Kafka doesn't have this. You'd need Redis or etcd or Consul separately. With NATS, it's built in.</p>
</blockquote>
<h3>6. Leaf Nodes — The Firewall Solver (revisited)</h3>
<p>As we explained earlier, Leaf Nodes let a small NATS server in a private network establish an outbound connection to your cloud hub. Once connected, the entire NATS feature set — pub/sub, request/reply, JetStream, KV store — works transparently across that connection.</p>
<p>Your cloud services don't even know (or care) that the customer's systems are behind a firewall. They just publish and subscribe to subjects, and NATS routes the messages intelligently.</p>
<hr />
<h2>Why Not Just Use Kafka for All of This?</h2>
<p>This is a fair question. Let's be direct about it.</p>
<h3>Kafka's strengths:</h3>
<ul>
<li>Handling millions of events per second (think: clickstream data, logs, telemetry)</li>
<li>Long-term event storage (days, months, years) with log compaction</li>
<li>Ecosystem integrations via Kafka Connect (Elasticsearch, S3, databases)</li>
</ul>
<h3>Where Kafka struggles in a SaaS context:</h3>
<p><strong>1. It assumes a shared network.</strong> Kafka brokers need to be reachable by all consumers and producers. This falls apart the moment a customer's systems are behind a firewall. There's no built-in solution — you end up layering VPNs, SSH tunnels, or custom proxies on top.</p>
<p><strong>2. It's expensive to run per-customer.</strong> If every enterprise customer needs their own Kafka cluster (because you can't share topics across security boundaries), you're looking at significant infrastructure costs and operational complexity for each customer onboarding.</p>
<p><strong>3. It's heavyweight for async communication.</strong> If you're only using Kafka for passing messages between microservices — not for high-throughput analytics — you're carrying a lot of operational weight for a relatively simple job. A NATS cluster uses a fraction of the memory and CPU.</p>
<p><strong>4. Request/Reply isn't native.</strong> Synchronous RPC patterns in Kafka require custom correlation ID implementations. NATS has this built in.</p>
<p><strong>5. No built-in KV or Object store.</strong> Features like distributed config and binary artifact storage require separate systems (Redis, S3, etcd) alongside Kafka.</p>
<hr />
<h2>The Architecture We Ended Up With</h2>
<p>Here's the high-level picture of how the pieces fit together in a SaaS context:</p>
<p><img src="https://cdn.hashnode.com/uploads/covers/6af72d371eb3c0fde6baf220/4a7f6ae4-ace4-44d0-b074-e5647288e565.png" alt="mermaid-diagram-2026-11-14-085636" /></p>
<p>Each customer runs a small NATS leaf node. It dials out to your hub. From there, everything — data sync, commands, configuration, acknowledgments — flows through that one outbound connection, fully encrypted.</p>
<hr />
<h2>What We Gained</h2>
<p>Switching from a Kafka-only architecture to NATS brought us real, measurable benefits:</p>
<p><strong>🔓 We could onboard customers behind firewalls</strong> — something that was simply impossible before. No VPN negotiation, no "open this port" emails to IT departments.</p>
<p><strong>📉 Infrastructure costs dropped</strong> — NATS uses a fraction of the compute that Kafka needs. For customers who need a local node, the NATS binary runs fine on a $5/month VM or even a Raspberry Pi.</p>
<p><strong>⚡ Latency improved</strong> — NATS is designed for low-latency messaging, not high-throughput batching. For real-time sync use cases, messages that took hundreds of milliseconds over Kafka were now single-digit milliseconds.</p>
<p><strong>🧩 We eliminated three separate systems</strong> — Kafka (messaging) + Redis (KV/config) + a custom RPC layer were replaced by a single NATS cluster with JetStream.</p>
<p><strong>🛡️ Security became simpler</strong> — Every connection in NATS is TLS by default. The customer's leaf node authenticates with cryptographic credentials. No passwords in config files, no shared secrets.</p>
<hr />
<h2>Should You Make the Switch?</h2>
<p>Not necessarily — and that's an honest answer.</p>
<p>If your primary use case is ingesting millions of rows of clickstream data, running Kafka Connect pipelines into your data warehouse, or powering an analytics platform, Kafka is the right tool. It was built for that.</p>
<p>But if your use case looks like this:</p>
<ul>
<li>Microservices that need reliable async communication</li>
<li>Real-time sync with customer systems behind firewalls</li>
<li>Low-latency event delivery rather than high-throughput batching</li>
<li>Dynamic configuration pushed to services without restarts</li>
<li>Keeping infrastructure simple and cheap to operate</li>
</ul>
<p>...then NATS is worth a serious look.</p>
<p>The firewall problem alone — connecting to customer private networks without asking them to open ports — is enough of a reason for many SaaS companies.</p>
<hr />
<h2>A Final Analogy</h2>
<p>If Kafka is a <strong>freight railway</strong> — massive capacity, fixed infrastructure, runs on its own dedicated tracks, needs a lot of setup — then NATS is a <strong>courier network</strong> — fast, lightweight, finds a way to deliver even when the front door is locked, and doesn't need you to build a railway station to use it.</p>
<p>Both are valuable. Know which job you're hiring for.</p>
<hr />
<h2>Where to Go From Here</h2>
<p>If this sparked your interest, here are the best places to learn more:</p>
<ul>
<li>📖 <a href="https://docs.nats.io">NATS Documentation</a> — clear, well-organized, with real examples</li>
<li>🧪 <a href="https://natsbyexample.com">NATS by Example</a> — runnable patterns in every language</li>
<li>🎥 <a href="https://www.youtube.com/@nats-io">NATS on YouTube</a> — architecture deep dives and demos</li>
</ul>
<hr />
<p><em>If this article helped you, share it with a teammate who's battling the same firewall problem. And if you've solved it differently, I'd genuinely love to hear about it in the comments.</em></p>
]]></content:encoded></item></channel></rss>