<?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: Danish Raza Bangash</title>
    <description>The latest articles on DEV Community by Danish Raza Bangash (@danishrazabangash).</description>
    <link>https://dev.to/danishrazabangash</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%2F4019486%2Ff5eb2cf2-c272-41c3-8547-2b1e9fda0ace.PNG</url>
      <title>DEV Community: Danish Raza Bangash</title>
      <link>https://dev.to/danishrazabangash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danishrazabangash"/>
    <language>en</language>
    <item>
      <title>Redis, Caching, and Queues: The Boring Stuff That Makes Apps Feel Fast</title>
      <dc:creator>Danish Raza Bangash</dc:creator>
      <pubDate>Tue, 04 Aug 2026 10:56:22 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/redis-caching-and-queues-the-boring-stuff-that-makes-apps-feel-fast-9g0</link>
      <guid>https://dev.to/danishrazabangash/redis-caching-and-queues-the-boring-stuff-that-makes-apps-feel-fast-9g0</guid>
      <description>&lt;p&gt;When I started building Footalyzer, I didn't think much about Redis. I had Next.js on the frontend, Express on the backend, MongoDB for data — that felt like enough. Then I started hitting real problems: slow API calls, repeated requests hammering an external football API, and AI-generated content that took too long to feel "live." That's when Redis, caching, and job queues stopped being buzzwords and became things I actually needed.&lt;/p&gt;

&lt;p&gt;Here's the simple version of what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  What even is Redis?
&lt;/h2&gt;

&lt;p&gt;Redis is basically a super-fast storage box that lives in memory (RAM) instead of on disk. Because RAM is way faster than a database like MongoDB, reading and writing to Redis takes microseconds instead of milliseconds.&lt;/p&gt;

&lt;p&gt;The catch: it's not meant to be your main database. It's meant to hold stuff temporarily — things you'll ask for again and again, or things you need to process in the background.&lt;/p&gt;

&lt;p&gt;That's really the two big uses: &lt;strong&gt;caching&lt;/strong&gt; and &lt;strong&gt;queues&lt;/strong&gt;. Different problems, same tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching: stop doing the same work twice
&lt;/h2&gt;

&lt;p&gt;Say a user opens Footalyzer and asks for a match briefing. Behind the scenes, that might mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calling an external football API for match data&lt;/li&gt;
&lt;li&gt;Calling an AI model to generate the analysis&lt;/li&gt;
&lt;li&gt;Formatting everything nicely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's slow and, if it's hitting a paid API, expensive too. Now imagine 50 people ask about the same match in the next hour. Do you really want to repeat all that work 50 times?&lt;/p&gt;

&lt;p&gt;This is where caching comes in. The first time someone asks, you do the work and save the result in Redis with an expiry time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`match:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:briefing`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;briefing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateBriefing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// slow, expensive&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`match:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:briefing`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;briefing&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// cache for 5 min&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;briefing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the next 49 people get an instant response, and you saved 49 API calls. That "EX 300" part just means "forget this after 5 minutes" — useful for live football data that changes during a match.&lt;/p&gt;

&lt;p&gt;The mental model that helped me: &lt;strong&gt;cache anything that's expensive to compute but doesn't change every second.&lt;/strong&gt; Match briefings, standings, player stats — all good candidates. Live scores that update every few seconds? Not so much, unless your expiry is really short.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other problem: things that take too long to do "live"
&lt;/h2&gt;

&lt;p&gt;Caching solves repeated work. But some work is just plain slow the first time too — like generating an AI briefing, sending emails, or processing a webhook from a payment provider. If you make the user wait for all of that inside a single request, your app feels frozen, and on a slow connection the request might just time out.&lt;/p&gt;

&lt;p&gt;The fix is to not do it "live" at all. Instead, you hand the task off to a queue, respond to the user immediately ("we're on it!"), and process the actual work in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter BullMQ
&lt;/h2&gt;

&lt;p&gt;BullMQ is a job queue library built on top of Redis. It's what actually makes "background jobs" possible in a Node.js app. The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Something happens (a user requests a briefing)&lt;/li&gt;
&lt;li&gt;You add a "job" to a queue instead of doing the work right there&lt;/li&gt;
&lt;li&gt;A separate "worker" picks up jobs from the queue and processes them, one at a time or several in parallel
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// adding a job&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;briefingQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generate-briefing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;matchId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// a worker processing it, elsewhere in the codebase&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;briefing-queue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;matchId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateBriefing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveBriefing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis is quietly doing the heavy lifting underneath — storing the queue, tracking which jobs are pending, done, or failed, and letting BullMQ retry jobs automatically if something breaks.&lt;/p&gt;

&lt;p&gt;This is where things clicked for me: &lt;strong&gt;caching makes reads fast, queues make writes/processing not block the user.&lt;/strong&gt; They solve different halves of the same "make the app feel instant" problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this plays out in Footalyzer
&lt;/h2&gt;

&lt;p&gt;In practice, a lot of it comes down to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt; the AI-generated briefings and stats pulls from the football API, since regenerating them for every request is wasteful and slow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue&lt;/strong&gt; the actual AI generation and any heavier background work, so a request that triggers it doesn't just hang&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Socket.io&lt;/strong&gt; to notify the frontend once a queued job finishes, instead of making the user refresh or poll&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this needed to be fancy from day one. I started with just caching a few slow endpoints, and added queues once I noticed requests were timing out during AI generation. That's probably the right order for most people — cache first, queue when you actually feel the pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few honest lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set expiry times on everything you cache.&lt;/strong&gt; Stale football data is worse than no cache at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't cache user-specific or fast-changing data&lt;/strong&gt; unless you really think it through — it's an easy way to serve someone the wrong info.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queues need monitoring.&lt;/strong&gt; A silently failing worker is worse than no queue, because now things are "processing" forever and nobody notices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis is cheap to add, easy to misuse.&lt;/strong&gt; It's tempting to cache everything. Start with the slowest, most repeated calls first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's really it. Redis isn't magic — it's just a very fast, temporary shelf. What you put on that shelf (cached results) and how you use it to hand off slow work (queues via BullMQ) is where the actual speed-up comes from.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>bullmq</category>
      <category>caching</category>
      <category>queues</category>
    </item>
    <item>
      <title>SQL vs NoSQL: A Simple Guide to Picking the Right Database</title>
      <dc:creator>Danish Raza Bangash</dc:creator>
      <pubDate>Sat, 01 Aug 2026 11:31:21 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/sql-vs-nosql-a-simple-guide-to-picking-the-right-database-5e6b</link>
      <guid>https://dev.to/danishrazabangash/sql-vs-nosql-a-simple-guide-to-picking-the-right-database-5e6b</guid>
      <description>&lt;p&gt;If you're building your first real app, you'll hit this question pretty fast: "Should I use SQL or NoSQL?"&lt;/p&gt;

&lt;p&gt;It sounds intimidating, but once you get the core idea, it's actually pretty simple. Let's break it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a database anyway?
&lt;/h2&gt;

&lt;p&gt;A database is just a place to store your app's data so it doesn't vanish the moment you close the browser. Think of it as a very organized filing cabinet. &lt;em&gt;How&lt;/em&gt; you organize that cabinet is where SQL and NoSQL split off.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL databases: structured and strict
&lt;/h2&gt;

&lt;p&gt;SQL databases (like PostgreSQL, MySQL, or SQLite) store data in &lt;strong&gt;tables&lt;/strong&gt; — rows and columns, like a spreadsheet. Every row follows the same structure, and tables can be linked to each other.&lt;/p&gt;

&lt;p&gt;Example: a simple &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;orders&lt;/code&gt; setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want all orders for a specific user? One clean query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'jane@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;JOIN&lt;/code&gt; is SQL's superpower. Relationships between data are enforced by the database itself, and querying across them is straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  NoSQL databases: flexible and fast
&lt;/h2&gt;

&lt;p&gt;NoSQL databases (like MongoDB, Firebase, or Redis) don't force your data into rigid tables. The most common type — document databases — store data as JSON-like objects, and each document can look different from the next.&lt;/p&gt;

&lt;p&gt;Example: the same "user with orders" idea in MongoDB.&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&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="s2"&gt;"0001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Jane"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orders"&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="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;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Keyboard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&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="p"&gt;]&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;Everything about Jane lives in one document. No joins needed — you just fetch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real differences
&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;SQL&lt;/th&gt;
&lt;th&gt;NoSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Structure&lt;/td&gt;
&lt;td&gt;Fixed tables, rows &amp;amp; columns&lt;/td&gt;
&lt;td&gt;Flexible documents/objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema&lt;/td&gt;
&lt;td&gt;Defined upfront&lt;/td&gt;
&lt;td&gt;Can change anytime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relationships&lt;/td&gt;
&lt;td&gt;Strong, via joins&lt;/td&gt;
&lt;td&gt;Usually nested or duplicated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling&lt;/td&gt;
&lt;td&gt;Mostly vertical (bigger server)&lt;/td&gt;
&lt;td&gt;Built for horizontal scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for&lt;/td&gt;
&lt;td&gt;Transactions, complex queries&lt;/td&gt;
&lt;td&gt;Fast iteration, huge write volume&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  So which one should you use?
&lt;/h2&gt;

&lt;p&gt;Honestly, it depends on what you're building.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building something involving money, inventory, or anything that needs strict consistency (banking, e-commerce checkout)? &lt;strong&gt;Go SQL.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Building something where the data shape changes often, or that needs to handle a ton of reads/writes fast (chat apps, activity feeds, content platforms)? &lt;strong&gt;NoSQL fits better.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the part most tutorials skip: you don't have to pick just one. A lot of real production apps use both — SQL for core transactional data, and something like Redis for caching or MongoDB for logs and analytics. This mix is sometimes called &lt;em&gt;polyglot persistence&lt;/em&gt;, and it's more common than people think.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;SQL vs NoSQL isn't really a battle — it's a tradeoff between structure and flexibility. Once you understand what each one is actually good at, the choice usually makes itself based on what you're building.&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>database</category>
      <category>sql</category>
      <category>mongodb</category>
    </item>
    <item>
      <title>Docker for MERN Developers: How I Actually Learned It (and Why It Clicked)</title>
      <dc:creator>Danish Raza Bangash</dc:creator>
      <pubDate>Sun, 12 Jul 2026 11:27:00 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/docker-for-mern-developers-how-i-actually-learned-it-and-why-it-clicked-35e5</link>
      <guid>https://dev.to/danishrazabangash/docker-for-mern-developers-how-i-actually-learned-it-and-why-it-clicked-35e5</guid>
      <description>&lt;p&gt;For the longest time, Docker was that thing on my resume-worthy-tools list that I hadn't really &lt;em&gt;earned&lt;/em&gt;. I knew the buzzwords — containers, images, &lt;code&gt;docker-compose&lt;/code&gt; — but I didn't know Docker the way you know a tool you've actually shipped something with.&lt;/p&gt;

&lt;p&gt;That changed while building &lt;strong&gt;BotForge&lt;/strong&gt;, my multi-tenant no-code AI chatbot builder. Here's how the pieces fell into place, and how Docker now fits naturally into my MERN workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "it works on my machine" wall
&lt;/h2&gt;

&lt;p&gt;BotForge isn't a single service. It's a MERN app with a Node/Express API, MongoDB, a four-tier RAG pipeline hitting Gemini embeddings, and integrations with WhatsApp and Telegram. Running all of that locally meant juggling environment variables, Node versions, and Mongo instances — and every time I handed the project to my teammate or tried deploying to Render, something that worked perfectly on my laptop broke somewhere else.&lt;/p&gt;

&lt;p&gt;That's the exact pain Docker exists to solve, and once I framed it that way, the concept stopped being abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model that made it click
&lt;/h2&gt;

&lt;p&gt;Forget the textbook definitions for a second. Here's the analogy that actually worked for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image&lt;/strong&gt; = a recipe. It lists every ingredient and step needed to make your app run — Node version, dependencies, source code, start command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container&lt;/strong&gt; = the dish made from that recipe. You can make the same dish a hundred times, on a hundred different stoves, and it comes out identical every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dockerfile&lt;/strong&gt; = the recipe card itself, written in a language Docker understands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once containers stopped feeling like "mini virtual machines" and started feeling like "a guaranteed-identical environment," everything else — layers, volumes, networking — became details I could look up as needed instead of concepts I had to memorize upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually learned it (step by step)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dockerized one thing first&lt;/strong&gt; — not the whole BotForge stack, just the Express API. A minimal &lt;code&gt;Dockerfile&lt;/code&gt;, one &lt;code&gt;EXPOSE&lt;/code&gt;, one &lt;code&gt;CMD&lt;/code&gt;. Got it running with &lt;code&gt;docker run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understood images vs. containers hands-on&lt;/strong&gt; by running the same image twice and watching two independent containers spin up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introduced &lt;code&gt;docker-compose&lt;/code&gt;&lt;/strong&gt; once I needed Mongo alongside the API — this is where Docker started feeling &lt;em&gt;useful&lt;/em&gt; instead of academic, because one &lt;code&gt;docker-compose up&lt;/code&gt; replaced a page of setup instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broke things on purpose&lt;/strong&gt; — killed containers mid-run, misconfigured ports, forgot &lt;code&gt;.dockerignore&lt;/code&gt; and watched &lt;code&gt;node_modules&lt;/code&gt; bloat my image. Debugging is where the concepts actually stick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recently&lt;/strong&gt;, I turned this whole progression into an interactive Docker learning page for other MERN devs walking the same path — writing it out taught me as much as building the thing did.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where Docker actually fits in a MERN stack
&lt;/h2&gt;

&lt;p&gt;This is the part a lot of tutorials skip: Docker isn't one container, it's usually &lt;strong&gt;several, talking to each other&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A typical MERN &lt;code&gt;docker-compose.yml&lt;/code&gt; looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./server&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5000:5000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MONGO_URI=mongodb://mongo:27017/botforge&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo&lt;/span&gt;

  &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./client&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;

  &lt;span class="na"&gt;mongo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo:7&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo-data:/data/db&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service — React frontend, Express API, MongoDB — gets its own container, its own isolated environment, and they talk to each other over Docker's internal network using service names instead of &lt;code&gt;localhost&lt;/code&gt;. For BotForge specifically, this meant my RAG pipeline, the API layer, and the database could all be versioned and deployed together, and the exact same setup that ran on my machine ran identically on Render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;The real win wasn't learning Docker commands — it was what it &lt;em&gt;removed&lt;/em&gt;: no more "install Node 18.x, then Mongo, then set these seven env vars" onboarding docs. Just &lt;code&gt;docker-compose up&lt;/code&gt;. For a multi-tenant app like BotForge, where consistency across environments actually matters, that's not a nice-to-have, it's the difference between a demo and something deployable.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're a MERN dev starting today
&lt;/h2&gt;

&lt;p&gt;Don't start with &lt;code&gt;docker-compose&lt;/code&gt;. Don't start with Kubernetes (please don't). Start by Dockerizing one Express route. Watch it run identically twice. Then add Mongo. Then add your frontend. The stack builds itself once the first container clicks — and it will click faster than the docs make it seem.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>mern</category>
      <category>devops</category>
      <category>node</category>
    </item>
    <item>
      <title>Building for Bursty Traffic: Queues and Real-Time Design for a World Cup App July 11, 2026</title>
      <dc:creator>Danish Raza Bangash</dc:creator>
      <pubDate>Sat, 11 Jul 2026 10:33:45 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/building-for-bursty-traffic-queues-and-real-time-design-for-a-world-cup-appjuly-11-2026-2fm9</link>
      <guid>https://dev.to/danishrazabangash/building-for-bursty-traffic-queues-and-real-time-design-for-a-world-cup-appjuly-11-2026-2fm9</guid>
      <description>&lt;p&gt;Sports traffic is a brutal test case for system design. Nothing happens for 89 minutes, then a goal fires and every connected client wants an update in the same second. I hit this directly building a World Cup 2026 companion app — live scores, match analysis, notifications — using Next.js, Socket.io, and Redis-backed job queues (BullMQ).&lt;/p&gt;

&lt;p&gt;This isn't a system handling millions of concurrent users. But the design questions that traffic pattern forces on you are the same ones you'd face at much larger scale — you just get more room to experiment before it actually breaks. Here's what I learned about async processing and real-time delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Async-first beats synchronous-and-hope
&lt;/h2&gt;

&lt;p&gt;Handling bursty traffic synchronously — API request comes in, app does the work, app responds — falls over exactly when it matters most, because your spikes are correlated with the thing your users actually care about. A missed goal notification during the one minute it mattered is worse than no feature at all.&lt;/p&gt;

&lt;p&gt;So anything that wasn't required to happen &lt;em&gt;in&lt;/em&gt; the request-response cycle got pushed into a queue: fetching and processing match events, fanning out notifications, running post-match analysis. The web server's job became "accept the request fast and hand off the work," not "do the work." Redis-backed queues gave me retry logic and backpressure almost for free — if a burst of events comes in faster than workers can process them, they queue instead of timing out or crashing the server.&lt;/p&gt;

&lt;p&gt;The tradeoff is real: async systems are harder to reason about, harder to debug (where did this job go?), and you now have eventual consistency to think about instead of "it just happened." But for anything bursty, that tradeoff is almost always worth it over a synchronous system that's simpler right up until the one moment it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Real-time doesn't mean "broadcast to everyone"
&lt;/h2&gt;

&lt;p&gt;Socket.io makes it dangerously easy to broadcast every update to every connected client. That works fine in a demo. It does not work once you have thousands of people watching different matches, because now every client's browser is filtering out most of the messages it receives just to find the one it cares about.&lt;/p&gt;

&lt;p&gt;The fix was scoping connections into rooms — one per match — so a client only receives events for what it's actually watching. This is an obvious idea in hindsight, but it's the kind of thing that's easy to skip when you're focused on "does real-time work at all," and only becomes a problem once your Socket.io server is doing meaningfully more work than it needs to per connected client.&lt;/p&gt;

&lt;p&gt;The general principle: &lt;strong&gt;real-time infrastructure scales by what you &lt;em&gt;don't&lt;/em&gt; send, not just by how fast you send it.&lt;/strong&gt; Scoping and targeting your broadcasts is usually a bigger scalability lever than optimizing the transport itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your real bottleneck is probably someone else's API
&lt;/h2&gt;

&lt;p&gt;The app leans on third-party services for live data, AI-powered analysis (Gemini), and billing (Stripe) — each with its own rate limits and failure modes that don't show up until traffic actually spikes. A burst of match events can mean a burst of downstream calls to generate analysis or trigger notifications, and none of those upstream services promise to absorb that burst gracefully just because your own queue can.&lt;/p&gt;

&lt;p&gt;That pushed rate-limiting and backoff logic down into the worker layer itself, not just the request layer — workers pulling jobs off the queue needed to respect external rate limits independently of how fast jobs were arriving. The queue smooths out &lt;em&gt;your&lt;/em&gt; bursts; it doesn't automatically smooth out the burst you're about to hand to someone else's API.&lt;/p&gt;

&lt;p&gt;The lesson: when you sketch your architecture diagram, the boxes representing external APIs deserve as much design attention as the boxes you control. Queueing your own work is necessary but not sufficient — you have to think about what your workers do when they hit someone else's limits too.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cheap infrastructure, disciplined architecture
&lt;/h2&gt;

&lt;p&gt;This system runs on free/low-tier hosting, not a fleet of load-balanced servers — because that's the reality of building as a student. That constraint was useful. It forced decisions that scale regardless of the infrastructure underneath: stateless services that don't care which instance handles a request, background work that lives in a queue instead of memory, and real-time connections scoped tightly enough that they don't waste bandwidth they don't need.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that most "scalability" problems people reach for infrastructure to solve are actually architecture problems. A tightly-coupled, synchronous system doesn't get more scalable by throwing more servers at it — it just gets more expensive in the same shape. Fixing the shape first, on cheap infrastructure, means that if I do need to scale the infrastructure later, I'm scaling something already designed to be scaled.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push bursty, non-critical-path work into queues&lt;/strong&gt; — synchronous systems fail exactly when traffic spikes matter most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope real-time broadcasts&lt;/strong&gt; — sending less is usually a bigger lever than sending faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate-limit at the worker layer, not just the request layer&lt;/strong&gt; — your queue smooths your own bursts, not the ones you hand downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the architecture before reaching for infrastructure&lt;/strong&gt; — cheap hosting with disciplined design scales further than you'd expect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this required expensive infrastructure or massive traffic to learn — it just required paying attention to &lt;em&gt;why&lt;/em&gt; a design choice mattered, not just whether it worked in the demo. That's the part that transfers, regardless of what you're building next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a final-year CS student building AI-integrated MERN stack applications. Part one of this series covers the multi-tenant RAG side of things, built for a very different kind of load — drop a comment if you've hit similar queueing or real-time tradeoffs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>systemdesign</category>
      <category>realtime</category>
      <category>node</category>
    </item>
    <item>
      <title>Designing a Multi-Tenant RAG Pipeline: Lessons From Building an AI Chatbot Platform</title>
      <dc:creator>Danish Raza Bangash</dc:creator>
      <pubDate>Fri, 10 Jul 2026 10:26:28 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/designing-a-multi-tenant-rag-pipeline-lessons-from-building-an-ai-chatbot-platform-149f</link>
      <guid>https://dev.to/danishrazabangash/designing-a-multi-tenant-rag-pipeline-lessons-from-building-an-ai-chatbot-platform-149f</guid>
      <description>&lt;p&gt;Most "scalable system design" articles describe theoretical systems handling millions of users that neither the writer nor the reader will ever actually build. This isn't that. This is what I actually learned architecting a multi-tenant AI chatbot builder — the kind of system where dozens of businesses share one backend, each with their own bots, knowledge bases, and messaging integrations — on a student budget, with free-tier infrastructure.&lt;/p&gt;

&lt;p&gt;The system doesn't serve millions of requests a day. But the design questions you hit at 100 requests a day are the same ones you hit at 100,000 — you just get more room to be wrong and fix it. Here's what I learned about retrieval-augmented generation (RAG) and multi-tenancy before I actually had scale to worry about.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Design for tenants before you design for traffic
&lt;/h2&gt;

&lt;p&gt;The chatbot builder needed to support many businesses (tenants), each with their own bots, knowledge bases, WhatsApp/Telegram connections, and usage limits — all on one shared backend.&lt;/p&gt;

&lt;p&gt;The tempting shortcut is to bolt tenancy on later: one database, a &lt;code&gt;userId&lt;/code&gt; field sprinkled into every collection, and hope nothing leaks. I went the other way and treated tenant isolation as a first-class part of the schema from day one — every document that mattered (bots, conversations, knowledge base chunks, subscription state) carried a tenant identifier that every query had to pass through, no exceptions.&lt;/p&gt;

&lt;p&gt;The tradeoff: it's slower to build features early on, because you can't cut corners on "whose data is this." The payoff: when you add a new feature later, you're not doing an emergency audit of every collection to figure out where tenant data might be leaking into another tenant's view. Isolation-by-default is one of those decisions that's nearly free at the start and extremely expensive to retrofit.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Not every request needs the same amount of work
&lt;/h2&gt;

&lt;p&gt;The chatbot's core feature is retrieval-augmented generation: a user asks a question, the system searches a knowledge base, and an LLM answers using the retrieved context. The naive version of this does one thing — embed the query, run a vector search, stuff results into a prompt — for every single request, regardless of how the question is phrased or what's actually being asked.&lt;/p&gt;

&lt;p&gt;That's wasteful. A one-word follow-up question and a complex multi-part query don't need the same retrieval effort, but they'll cost the same if you treat them identically.&lt;/p&gt;

&lt;p&gt;I ended up building the retrieval pipeline in tiers — cheaper, faster paths handle requests that don't need deep retrieval, and progressively heavier tiers only kick in when the query actually warrants it. In production this pipeline consistently retrieves relevant context with around 90% accuracy at roughly 780ms average latency — good enough to feel responsive in a chat interface, without paying full retrieval cost on every message.&lt;/p&gt;

&lt;p&gt;The general lesson isn't "build a four-tier pipeline." It's: &lt;strong&gt;look for the cases where you're doing expensive work uniformly across requests that don't uniformly need it.&lt;/strong&gt; That gap is usually where your easiest scalability wins live, and it's almost always cheaper than reaching for more infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Your real bottleneck is probably someone else's API
&lt;/h2&gt;

&lt;p&gt;The platform leans heavily on third-party APIs — Gemini for embeddings and generation, WhatsApp's Cloud API and Telegram's Bot API for messaging. These have their own rate limits, latency, and failure modes that you don't control, and at small scale it's easy to forget they're there until a burst of traffic hits one at once.&lt;/p&gt;

&lt;p&gt;Multi-tenant WhatsApp integration made this concrete: each business brings its own WhatsApp number via Meta's Embedded Signup flow, but the app still has to be a good citizen of Meta's platform-wide rate limits across every tenant combined, not just per-tenant. That means rate limiting and backoff logic had to be designed at the application layer, not assumed away because "each tenant has their own number."&lt;/p&gt;

&lt;p&gt;The lesson: when you sketch your architecture diagram, the boxes representing external APIs deserve as much design attention as the boxes you control. They will rate-limit you, they will have outages, and they will not always fail gracefully. Plan for it before you need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Cheap infrastructure, disciplined architecture
&lt;/h2&gt;

&lt;p&gt;This system runs on free/low-tier hosting (Render), not a fleet of load-balanced servers — because that's the reality of building as a student. That constraint was useful. It forced decisions that scale regardless of the infrastructure underneath them: strict tenant isolation, tiered retrieval instead of brute-force retrieval, and rate-limit-aware integrations instead of naive ones.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that most "scalability" problems people reach for infrastructure to solve are actually architecture problems. Fixing the shape of the system first, on cheap infrastructure, means that if I do need to scale the infrastructure later, I'm scaling something already designed to be scaled — not retrofitting scalability onto something that was never built for it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolate tenants at the schema level from day one&lt;/strong&gt; — retrofitting isolation later means auditing everything you've already built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't do uniform work for non-uniform requests&lt;/strong&gt; — tiering retrieval effort to what a query actually needs is often a bigger win than adding infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for the external APIs you don't control&lt;/strong&gt; — they're often the actual bottleneck, not your own code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the architecture before reaching for infrastructure&lt;/strong&gt; — cheap hosting with disciplined design scales further than you'd expect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this required expensive infrastructure or massive traffic to learn — it just required paying attention to &lt;em&gt;why&lt;/em&gt; a design choice mattered, not just whether it worked in the demo. That's the part that transfers, regardless of what you're building next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a final-year CS student building AI-integrated MERN stack applications. Part two of this series covers the async and real-time side of things, built for a very different kind of load — drop a comment if you've hit similar RAG or multi-tenancy tradeoffs.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>node</category>
    </item>
    <item>
      <title>Building a Four-Tier Parallel RAG Pipeline with Gemini</title>
      <dc:creator>Danish Raza Bangash</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:39:27 +0000</pubDate>
      <link>https://dev.to/danishrazabangash/building-a-four-tier-parallel-rag-pipeline-with-gemini-3lpa</link>
      <guid>https://dev.to/danishrazabangash/building-a-four-tier-parallel-rag-pipeline-with-gemini-3lpa</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When building BotForge, our AI no-code chatbot platform, we needed a retrieval system that could handle messy, real-world user queries — typos, partial phrases, semantically similar-but-differently-worded questions.&lt;/p&gt;

&lt;p&gt;A naive vector search alone wasn't good enough. It's powerful but brittle to out-of-vocabulary terms and exact keyword lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Four-Tier Parallel Retrieval
&lt;/h2&gt;

&lt;p&gt;We ran four retrieval strategies &lt;strong&gt;simultaneously&lt;/strong&gt; using &lt;code&gt;Promise.all\&lt;/code&gt;, then merged results with a weighted scoring function.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;javascript&lt;br&gt;
const [semanticResults, textResults, regexResults, fuzzyResults] = await Promise.all([&lt;br&gt;
  semanticSearch(query, embeddings),      // weight 1.8x&lt;br&gt;
  mongoFullTextSearch(query),             // weight 1.5x&lt;br&gt;
  regexKeywordSearch(query),              // weight 1.0x&lt;br&gt;
  fuzzyPerWordMatch(query),               // weight 0.6x&lt;br&gt;
])&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 1: Semantic Search (1.8× weight)
&lt;/h3&gt;

&lt;p&gt;Using Gemini &lt;code&gt;gemini-embedding-2\&lt;/code&gt; to produce &lt;strong&gt;3072-dimensional vectors&lt;/strong&gt;, we compute cosine similarity against stored document embeddings. This catches meaning — "how do I reset my login?" matches "account recovery options" even with no shared words.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 2: MongoDB Full-Text Search (1.5× weight)
&lt;/h3&gt;

&lt;p&gt;A native MongoDB Atlas text index for fast, exact keyword hits. Great for technical terms, product names, and precise phrases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 3: Regex Keyword Matching (1.0× weight)
&lt;/h3&gt;

&lt;p&gt;Each significant word in the query is compiled to a case-insensitive regex. Catches partial matches and hyphenated variants.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 4: Fuzzy Per-Word Matching (0.6× weight)
&lt;/h3&gt;

&lt;p&gt;Levenshtein distance matching per query word — handles typos and misspellings like "configuraton" → "configuration".&lt;/p&gt;

&lt;h2&gt;
  
  
  Weighted Score Merging
&lt;/h2&gt;

&lt;p&gt;Each result carries a base score from its retrieval strategy. We deduplicate by chunk ID, sum scores across strategies, and sort descending:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;\&lt;/code&gt;&lt;code&gt;javascript&lt;br&gt;
function mergeResults(tiers, weights) {&lt;br&gt;
  const scoreMap = new Map()&lt;br&gt;
  tiers.forEach((results, i) =&amp;gt; {&lt;br&gt;
    results.forEach(({ id, score, chunk }) =&amp;gt; {&lt;br&gt;
      const weighted = score * weights[i]&lt;br&gt;
      scoreMap.set(id, {&lt;br&gt;
        chunk,&lt;br&gt;
        total: (scoreMap.get(id)?.total || 0) + weighted,&lt;br&gt;
      })&lt;br&gt;
    })&lt;br&gt;
  })&lt;br&gt;
  return [...scoreMap.values()].sort((a, b) =&amp;gt; b.total - a.total).slice(0, 5)&lt;br&gt;
}&lt;br&gt;
\&lt;/code&gt;&lt;code&gt;\&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~90% retrieval accuracy&lt;/strong&gt; on held-out test queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~780ms median latency&lt;/strong&gt; end-to-end (including embedding generation)&lt;/li&gt;
&lt;li&gt;Robust to typos, paraphrasing, and domain-specific terminology&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parallel architecture was the key insight — running all four strategies concurrently keeps latency close to the slowest individual strategy (semantic search) rather than multiplying it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cache embeddings for frequently asked questions&lt;/li&gt;
&lt;li&gt;Add a re-ranking step (cross-encoder) for the top 10 candidates&lt;/li&gt;
&lt;li&gt;Explore ColBERT-style late interaction for finer-grained scoring&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>mongodb</category>
      <category>node</category>
    </item>
  </channel>
</rss>
