{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreih3v5pgjqqlilglylibs2jbvgdv2msgo747egyfusmcndeqyniwdi",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpqbzttyook2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreibew7tpfme364jnsjvfohg7julwsdx5b3tczoib2m2xwmnoy32b4y"
    },
    "mimeType": "image/webp",
    "size": 476260
  },
  "path": "/surajrkhonde/the-shop-on-the-corner-how-i-learned-system-design-without-building-clone-54pa",
  "publishedAt": "2026-07-03T09:18:42.000Z",
  "site": "https://dev.to",
  "tags": [
    "systemdesign",
    "beginners",
    "computerscience",
    "webdev"
  ],
  "textContent": "#  The Shop on the Corner: How I Learned System Design Without Building Amazon\n\n_Nephew asks his uncle β€” 10 years deep into building large-scale systems β€” to explain \"system design,\" and all the scary jargon that comes with it. Uncle refuses to start with the jargon. Instead: \"Forget servers and databases for a minute. Just imagine you're running a small shop.\" What follows is a thought experiment, built one problem at a time β€” no cloud bills, no fancy stack, just a counter, a storeroom, and a lot of common sense._\n\n##  Part 1: The Counter β€” Your First \"API\"\n\nπŸ‘¦ **Nephew:** Uncle, everyone at work keeps throwing around terms β€” load balancer, cache, index, sharding. I nod along, but I don't actually _get_ any of it.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Then don't start there. Close your eyes for a second and forget servers exist. Suppose β€” just suppose β€” you're running a small shop. One counter, one small storeroom at the back. A customer walks up and asks for something. What do you do?\n\nπŸ‘¦ **Nephew:** I'd walk into the storeroom, find it, walk back, hand it over.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's it. That's the _entire_ job of a server handling a request. You don't need to understand Amazon's warehouse to understand Amazon's problems. You need a counter and a storeroom, imagined clearly, and the patience to grow them one honest problem at a time.\n\nHere's the shape of what you just described, whether you realized it or not:\n\n\n\n    🧍 Customer            πŸ§‘ You (Counter)           πŸ“¦ Storeroom (Database)\n        |                        |                              |\n        |── \"Got Maggi?\" ───────>|                              |\n        |                        |──── Walk in, search ────────>|\n        |                        |<─────── Found it ────────────|\n        |<── Hand it over, ──────|                              |\n        |     take payment       |                              |\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** One customer, one request, one trip to the storeroom, one response. This is fine. This is _correct_ , even, for a shop with five customers a day. Don't let anyone tell you a single counter isn't \"scalable\" β€” a shop that small doesn't need two counters, it needs someone to stop worrying and open the shutter.\n\nπŸ‘¦ **Nephew:** So this is just... a server handling one request at a time?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Exactly. Customer sends a request β†’ the app checks the storeroom (the database) β†’ brings back the data β†’ sends the response. No shortcuts, no cleverness needed yet. Every system, no matter how huge, starts as this one loop.\n\n##  Part 2: Footfall Increases β€” And Queues Appear\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Now, keep imagining. Your little shop is reliable β€” open every single day, rarely shuts without notice. Word gets around. What happens to footfall?\n\nπŸ‘¦ **Nephew:** It goes up, obviously. People trust a shop that's always open.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Good β€” write that down as lesson one. **Being consistently available is itself a feature.** In our world we call it uptime β€” 99.99% of the time, the shutter is up and someone's behind the counter. Customers don't reward the fanciest shop. They reward the one that's _always there._\n\nNow β€” more customers means what, at the counter?\n\nπŸ‘¦ **Nephew:** A queue. If I'm busy checking the storeroom for one person, the next person just has to stand and wait.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** And why is that dangerous? Think business, not tech.\n\nπŸ‘¦ **Nephew:** Because there's probably a Ramesh Kaka's shop right next door, selling the same things at the same price. If someone has to wait, they'll just walk in there instead.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's the entire business case for concurrency, in one sentence. It's never really about \"can the system handle load\" β€” it's about **what the customer experiences while they wait.** So β€” you're the shopkeeper now. What's your fix?\n\nπŸ‘¦ **Nephew:** Hire a helper. I stay at the counter, take the next order, handle billing. My helper runs to the storeroom and fetches. I don't freeze while he's fetching.\n\n\n\n    Customer 1        You (Counter)        Helper          Storeroom      Customer 2\n        |                   |                  |                |              |\n        |─\"2kg rice\"───────>|                  |                |              |\n        |                   |──Go fetch it────>|                |              |\n        |                   |   (you don't stand idle...)        |              |\n        |                   |<───────────────────────────────────\"Got salt?\"───|\n        |                   |──\"Yes, one sec\"─────────────────────────────────>|\n        |                   |                  |──Fetches──────>|              |\n        |                   |                  |<────Rice───────|              |\n        |                   |<──Rice ready─────|                |              |\n        |<─Hands over rice,─|                  |                |              |\n        |  billing done     |                  |                |              |\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** You just designed **non-blocking I/O** β€” the exact idea behind Node.js's event loop. You, the counter, are the _single thread_. You never block yourself waiting for something slow (the storeroom trip). You hand it off, keep talking to the next customer, and get notified the moment it's ready.\n\n\n\n    // What that shop is doing, in code terms\n    app.post('/order', async (req, res) => {\n      const item = await storeroom.fetch(req.body.itemName); // helper goes, you don't block\n      res.json({ item, billed: true });\n    });\n    // While `await` is \"in flight\" for one customer, the event loop\n    // is free to accept and start handling the NEXT customer's request.\n\n\nπŸ‘¦ **Nephew:** So a helper _is_ basically the async model.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Almost exactly. One thread stays responsive at all times, dispatches slow work to someone else (an I/O call), and picks the result back up when ready β€” instead of freezing the whole shop for one order.\n\n##  Part 3: The Nightly Habit β€” Reading Your Own Logs\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Now here's the habit that separates a shopkeeper from an _engineer_. Suppose you've built this discipline: every night before closing, you sit for ten minutes and go over the day. How long each order took, where the helper got delayed, which items got asked for most.\n\nπŸ‘¦ **Nephew:** Why bother, if nothing's actually broken?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's exactly the trap most people fall into β€” they only look when something's on fire. You look _every single night_ , whether anything broke or not. That's the difference between fire-fighting and actually understanding your own shop. It's what we call reading your **metrics and logs.**\n\nSo β€” one night, in this little thought experiment of ours, you notice something odd in the numbers. The helper is taking way too long, even for simple five-item orders. You ask him why. What do you think he says?\n\nπŸ‘¦ **Nephew:** Maybe... he can't find things quickly? Everything's scattered?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Exactly that. He tells you: \"Sir, I'm moving fast, but once I'm inside, everything is scattered. I don't know where the _mirchi powder_ is, where the _aachar oil_ is. I search the whole room every single time.\" What does that sound like to you?\n\nπŸ‘¦ **Nephew:** He's checking every shelf, one by one, every single time, just to find one item.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** In database terms, that's a **full table scan** β€” no index, so every query checks every row.\n\n\n\n    πŸƒ Helper enters\n          |\n          v\n    [ Checks shelf 1... shelf 2... shelf 3... shelf 4... ]   <-- every shelf, every time\n          |\n          v\n    Finally finds mirchi powder\n\n\n##  Part 4: The Rack System β€” Building Your First Index\n\nπŸ‘¨β€πŸ¦³ **Uncle:** So, as the shopkeeper in this story β€” what would you do to fix it?\n\nπŸ‘¦ **Nephew:** Organize the storeroom. Put spices on one rack, grains on another, oils on a third. And keep a notebook β€” item name, and which rack and shelf it's on.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's exactly what a good engineer would build here β€” and what you just described, with your hands, is precisely what a **database index** does with data. The racks are your data, sorted and grouped logically. The notebook is the **index** β€” a small, fast-to-scan lookup table that tells you _exactly_ where to go, instead of searching everything.\n\n\n\n    Without an index (full scan):\n      Look at every item β†’ O(n) β€” gets slower as the storeroom grows\n\n    With an index (the notebook):\n      Look up \"mirchi powder\" in notebook β†’ get \"Rack A, Shelf 2\"\n      β†’ Walk straight there β†’ O(log n) or better\n\n\n\n    -- What you just designed, in database terms\n    CREATE INDEX idx_item_name ON storeroom (item_name);\n\n    -- Before: database scans every single row to find \"mirchi powder\"\n    -- After: database jumps almost straight to it, using the index\n\n\nπŸ‘¦ **Nephew:** So the notebook is basically... a B-tree index?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Conceptually, yes. Most database indexes (B-tree, which Postgres and MySQL use by default) work exactly like your notebook: a sorted structure that lets the engine narrow down \"where\" in a handful of steps instead of checking every row. You didn't need to know the phrase \"B-tree\" to design the _idea_ correctly. That's the whole point of this exercise.\n\n##  Part 5: One Notebook Isn't Enough β€” Composite Indexes\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Let's push the thought experiment further. Some weeks later, in this imaginary shop, the helper is _still_ slow on certain orders β€” even with the racks and the notebook. Any guess why?\n\nπŸ‘¦ **Nephew:** Hmm. Maybe some items on the same rack still take time to find _within_ that rack?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Correct instinct. Say \"washing powder\" and \"cloth washing soap\" are both on the same rack. The notebook told the helper the _rack_ , but not exactly _where on the rack_ β€” so once he arrives, he's back to shelf-by-shelf searching, just within a smaller area. What would you fix?\n\nπŸ‘¦ **Nephew:** Update the notebook β€” two columns instead of one. Rack, and then exact shelf position within that rack.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's a **composite index** β€” an index built on more than one column, in a deliberate order.\n\n\n\n    -- Instead of indexing just item_name...\n    CREATE INDEX idx_item_name ON storeroom (item_name);\n\n    -- ...you index rack first, then item name within that rack\n    CREATE INDEX idx_rack_item ON storeroom (rack_id, item_name);\n\n    -- Now a query like \"everything on rack A, find soap first\"\n    -- uses the index far more efficiently than a single-column index ever could.\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** The order of columns matters, just like the order in your notebook. `(rack_id, item_name)` helps when you're filtering by rack first. Written the other way β€” `(item_name, rack_id)` β€” it helps differently. Real database engines make you choose this order deliberately, based on how you actually query the data.\n\n##  Part 6: The Cost Nobody Talks About β€” Indexing Isn't Free\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Now think about the other side. In this shop, new stock arrives β€” say, fifty new items in one evening. What happens now, with racks and a notebook to maintain?\n\nπŸ‘¦ **Nephew:** Restocking is slower. You have to find the right shelf, update the notebook, maybe rearrange nearby items so the ordering still makes sense.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Exactly β€” and that's the classic **read vs. write tradeoff** of indexing. An index makes _reads_ (lookups) fast, but every _write_ (insert, update, delete) now carries extra work β€” the index has to stay in sync with the actual data.\n\n| No Index | With Index\n---|---|---\n**Reads (lookup)** | Slow β€” scans everything | Fast β€” jumps straight there\n**Writes (add/update stock)** | Fast β€” just place it anywhere | Slower β€” index must be updated too\n**Best for** | Write-heavy, rarely-searched data | Read-heavy data, searched often\n\nπŸ‘¦ **Nephew:** So if the shop gets way more _orders_ (reads) than _stock deliveries_ (writes), building the index is still worth it?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Precisely β€” that's the right tradeoff for _that_ traffic pattern. It's the same judgment call a backend engineer makes before slapping an index on every column \"just in case.\" An index you rarely query but constantly update is pure overhead, nothing more.\n\n##  Part 7: Demand Grows Again β€” Time for a Cache\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Racks, notebook, composite index β€” solid shop now, in our little thought experiment. But footfall keeps growing. Even with a well-organized storeroom, what's still slow?\n\nπŸ‘¦ **Nephew:** The helper still has to physically walk to the storeroom every single time β€” even for the five or six items basically everyone asks for. Shampoo sachets, biscuits, small soap bars.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** So β€” you're the shopkeeper again. How would you fix _that_?\n\nπŸ‘¦ **Nephew:** Check the logs, find out which items are asked for most, and keep those on a small shelf right behind the counter β€” so I don't even send the helper for them, I just grab it myself.\n\n\n\n    🧍 Customer asks for shampoo\n                |\n                v\n        On the counter shelf?\n           /            \\\n         Yes              No\n          |                |\n          v                v\n    βœ… Hand it over    πŸƒ Send helper to storeroom\n       instantly            |\n                             v\n                      Fetch, then hand over\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That counter shelf is your **cache** β€” a small, fast copy of your _most-requested_ data, kept close by so you skip the slow trip entirely. And notice how you picked what goes on it: watch the logs, find the high-demand items. That's exactly how real systems decide cache eviction and warm-up policy. Popular data lives in something like Redis, close to the app; everything else stays in the \"storeroom\" β€” the real database.\n\n\n\n    // The shelf-behind-the-counter, in code\n    async function getItem(name) {\n      let item = cache.get(name);         // check the counter shelf first\n      if (item) return item;              // instant β€” cache hit\n\n      item = await storeroom.fetch(name); // cache miss β€” send the helper\n      cache.set(name, item);              // restock the shelf for next time\n      return item;\n    }\n\n\nπŸ‘¦ **Nephew:** And this would make things faster overnight.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** For a while. Then, in this same thought experiment β€” imagine something goes wrong one evening. Let's walk through it.\n\n##  Part 8: The Betrayal β€” When the Cache Lies to You\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Picture this: you've just added new stock, updated the counter shelf too β€” but customers are _still_ being told \"not available,\" even right after restocking. And stranger still, some items are being fetched from the storeroom even though they're specifically supposed to be sitting on the counter shelf. What's going on?\n\nπŸ‘¦ **Nephew:** I genuinely don't know. That sounds like a bug in the shelf itself.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Let's not guess β€” let's read the logs, like a good shopkeeper would. Suppose the logs show it's happening for exactly three items: milk, bread, eggs. What's special about those, compared to shampoo and biscuits?\n\nπŸ‘¦ **Nephew:** They... expire. Shampoo doesn't go bad in two days. Milk does.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Exactly. Now imagine the shop had set an expiry on counter-shelf items β€” what we call a **TTL, Time To Live** β€” so stock doesn't sit there forever pretending to be fresh. For milk and bread, that TTL is short. It expires _fast_ , falls off the shelf, and β€” good news β€” the system correctly falls back to the storeroom instead of just telling the customer \"sorry, not available.\"\n\n\n\n    // Setting a short TTL for perishables, longer for shelf-stable items\n    cache.set('milk', stock, { ttl: '2 days' });     // expires fast β€” risk of staleness\n    cache.set('shampoo', stock, { ttl: '30 days' }); // barely changes β€” safe to cache longer\n\n\nπŸ‘¦ **Nephew:** Okay, so the fallback is working as intended. What's the bad news, then?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** The bad news: the _restocking process_ itself is broken. When new milk arrives, the storeroom (the database) gets updated β€” but the counter shelf (the cache) doesn't. So even fresh milk that just arrived isn't reflected on the shelf until the old TTL naturally runs out. Customers, and the helper, are both being told stale information for however long that window lasts.\n\n##  Part 9: Fixing It Properly β€” Three Ways to Keep the Shelf Honest\n\nπŸ‘¦ **Nephew:** Okay, so if I were actually running this shop β€” how do I make sure the counter shelf and the storeroom never disagree?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** There are a few well-known patterns for exactly this. Real systems pick one _deliberately_ β€” not by accident. Let's walk through all three, because you'll meet all three again in your career.\n\n###  1. Cache-Aside (Lazy Loading) β€” the one you'll reach for 90% of the time\n\nWhen new stock arrives, you don't touch the counter shelf at all β€” you just **remove** whatever's currently there for that item. The _next_ customer who asks triggers a normal cache miss: helper goes to the storeroom, fetches the fresh item, and _that_ refills the shelf.\n\n\n\n    🚚 New Stock       πŸ“¦ Storeroom       πŸ›’οΈ Counter Shelf        🧍 Next Customer\n        |                    |                    |                       |\n        |──Update with───────>|                    |                       |\n        |   fresh milk        |                    |                       |\n        |──Remove old \"milk\" entry───────────────>|                       |\n        |                    |   (shelf stays empty for milk                |\n        |                    |    until next customer asks)                |\n        |                    |                    |<──\"Milk please\"────────|\n        |                    |                    |──Miss, not there──────>|\n        |                    |<───Fetch fresh milk─|                       |\n        |                    |───Fresh milk───────>|                       |\n        |                    |                    |──Restock shelf         |\n        |                    |                    |──Hand over milk───────>|\n\n\n\n    async function restock(item, newStock) {\n      await storeroom.update(item, newStock); // update source of truth first\n      await cache.delete(item);               // invalidate β€” don't refill yet\n    }\n    // The next GET naturally repopulates the cache (see getItem() from Part 7)\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** This is simple, and it's _self-healing_ β€” even if you get an invalidation wrong once, the TTL eventually cleans it up. The one cost: the very first customer after a restock pays a slightly slower \"cache miss\" trip. Totally acceptable in almost every shop, real or digital.\n\n###  2. Write-Through β€” update both, together, every time\n\nEvery time stock changes, you update the counter shelf **and** the storeroom in the same breath, before moving on to anything else.\n\n\n\n    async function restockWriteThrough(item, newStock) {\n      await cache.set(item, newStock);        // update shelf first\n      await storeroom.update(item, newStock); // then storeroom\n    }\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** The shelf is _never_ stale β€” but every single restock now takes a little longer, because you're doing two writes, always, whether or not anyone's about to ask for that item. Good for things you read constantly and can't afford to serve stale, even for one customer.\n\n###  3. Write-Behind (Write-Back) β€” update the shelf now, storeroom later\n\nYou update the counter shelf immediately, and let the storeroom catch up in the background, in a batch, a little later.\n\n\n\n    async function restockWriteBehind(item, newStock) {\n      await cache.set(item, newStock);                    // instant\n      queue.add('sync-to-storeroom', { item, newStock });  // storeroom updates later, async\n    }\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Fastest of the three for writes, but riskiest β€” if something crashes before the background sync runs, the storeroom and the shelf disagree, and you've lost the \"source of truth\" guarantee. This one's rare, and only for cases where raw write speed matters more than perfect consistency.\n\n###  Choosing Between Them\n\nPattern | Storeroom updated | Shelf updated | Risk | Use when\n---|---|---|---|---\n**Cache-aside** | Immediately | On next read (lazy) | Brief staleness for first reader | Default choice β€” most reads, occasional writes\n**Write-through** | Immediately | Immediately, same operation | Slower writes | Reads must never be stale\n**Write-behind** | Delayed (async) | Immediately | Data loss if crash before sync | Extremely write-heavy, staleness tolerable\n\nπŸ‘¨β€πŸ¦³ **Uncle:** For this shop β€” for most shops, honestly, and most real apps β€” **cache-aside** is the answer. It's simple, forgiving of mistakes, and what you'll reach for by default. You only pick write-through or write-behind when you have a _specific_ , _measured_ reason to.\n\nπŸ‘¦ **Nephew:** So the \"bug\" in our thought experiment wasn't that caching is a bad idea. It's that the shop was doing cache-aside _by accident_ β€” invalidating sometimes, without a clear rule.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's the real lesson tonight. Every pattern we've walked through β€” the index, the composite index, the cache, the TTL β€” none of it is complicated once you name it. What's complicated is doing it _accidentally_ , without knowing which tradeoff you actually picked. Name your pattern on purpose, and you debug it in five minutes instead of an angry evening staring at logs. That's the whole trick to \"system design\" β€” it was never about the jargon. It's about a shop, a counter, and asking \"what happens when more people show up\" one honest question at a time.\n\n##  Part 10: Why Stop at Masalas? β€” Diversifying, and a New Kind of Storage\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Everything's running smooth now. So β€” natural next question for any shopkeeper. Why only sell masalas? Why not vegetables too? More profit, and you stop depending on one single product line.\n\nπŸ‘¦ **Nephew:** Makes sense. We already have the storeroom β€” we can just place vegetables there too.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Careful. That storeroom was built for dry goods β€” packets, boxes, jars. Vegetables need a _freezer_ β€” a completely different kind of storage, built specifically for a different kind of \"item.\" In system terms, that's the difference between your regular database and **object storage** β€” something like Amazon S3. You don't put images, videos, or PDFs in the same place you put structured rows of text data. Those are large, unstructured _files_ β€” they need their own storage, built for exactly that shape of data.\n\nπŸ‘¦ **Nephew:** So we buy a whole separate storage unit?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Not buy β€” _rent_. Just like you wouldn't build your own cold-storage warehouse on day one, you don't run your own file-storage infrastructure on day one either. You rent space in someone else's β€” that's exactly what S3 is: rented, purpose-built object storage, priced by how much you use.\n\n\n\n    Regular Storeroom (Database)          Freezer Unit (Object Storage / S3)\n    ─────────────────────────             ──────────────────────────────────\n    Structured rows: name, price,         Unstructured blobs: images, videos,\n    qty, category                         PDFs, large files\n    Fast to query/search                  Fast to store/retrieve by \"key\"\n    Small items, lots of them             Large files, fewer of them\n    Built into your main DB               A separate rented service, on demand\n\n\n##  Part 11: The Current Overload β€” Storage Isn't Free to Move\n\nπŸ‘¦ **Nephew:** Great, vegetables are in the freezer, we can start selling from tomorrow!\n\n_(That evening, Nephew looks tense.)_\n\nπŸ‘¨β€πŸ¦³ **Uncle:** What happened? You look worried.\n\nπŸ‘¦ **Nephew:** The vegetable stock is choking my real customers. When we were loading everything into the freezer at once, it consumed so much power and bandwidth that regular orders couldn't get through β€” there was no room left to even handle other requests.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Calm down β€” this is a very common, very fixable problem. First: apply limits, like a small shop should. You don't need to onboard 10 acres of tomatoes on day one. Restrict it β€” say, 5 crates max per upload β€” and define a strict list of what you'll even accept.\n\nπŸ‘¦ **Nephew:** Okay, but even 5 crates of tomatoes, 6 of carrots... it still chokes the bandwidth when it all goes in together.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Right β€” that's a second, separate problem. Limiting _how much_ helps, but you also need to control _how_ it goes in.\n\n\n\n    // Step 1: restrict what you even accept\n    const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf', 'video/mp4'];\n    const MAX_SIZE_MB = 25;\n\n    function validateUpload(file) {\n      if (!ALLOWED_TYPES.includes(file.mimeType)) {\n        throw new Error('File type not accepted');\n      }\n      if (file.sizeMB > MAX_SIZE_MB) {\n        throw new Error('File too large');\n      }\n    }\n\n\n##  Part 12: Streaming Uploads β€” Don't Send the Whole Truck at Once\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Now the second fix. Instead of one truck showing up and dumping all 5 crates into the freezer at once β€” overwhelming your dock, your workers, everything β€” you make the truck unload _one crate at a time_ , in a steady stream. That way your dock stays clear enough to also handle regular customers walking in.\n\nπŸ‘¦ **Nephew:** So instead of one giant delivery all at once...\n\nπŸ‘¨β€πŸ¦³ **Uncle:** ...you use a **streaming upload** β€” sometimes called a **chunked** or **multipart upload**. The file gets broken into small pieces and sent piece by piece, instead of loading the entire thing into memory in one shot.\n\n\n\n    X All at once (buffers everything in memory first):\n    [################################################] -> BOOM, blocks everything else\n\n    OK Streamed, chunk by chunk:\n    [###] -> process -> [###] -> process -> [###] -> process -> done\n       (other requests can still squeeze through between chunks)\n\n\n\n    // Streaming a large file upload instead of loading it whole into memory\n    const fs = require('fs');\n\n    app.post('/upload', (req, res) => {\n      const writeStream = fs.createWriteStream(`/uploads/${req.headers['file-name']}`);\n      req.pipe(writeStream); // data flows in small chunks, not all at once\n\n      writeStream.on('finish', () => res.status(201).json({ message: 'Uploaded' }));\n      writeStream.on('error', (err) => res.status(500).json({ error: err.message }));\n    });\n\n\nπŸ‘¦ **Nephew:** So this is possible with my Node shop too?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Yes β€” Node's streams are built exactly for this. Small pieces flowing continuously keep your dock β€” your server's memory and bandwidth β€” clean and available for everyone else, instead of one giant truck blocking the entire gate.\n\n##  Part 13: Growing Pains β€” Splitting the Shop by Responsibility\n\n_(That evening, Uncle walks through Nephew's whole setup.)_\n\nπŸ‘¨β€πŸ¦³ **Uncle:** You need a bit more discipline in how this shop is structured. Right now, masalas, vegetables, payments, user registration β€” everything is mixed into one big room. Organize it by responsibility instead. Payments get their own counter and their own record book. Vegetables get their own. Registration gets its own. Each department owns its own files, end to end.\n\nπŸ‘¦ **Nephew:** I don't want a completely separate shop or building for payments and everything β€” that's unmanageable for a shop this size.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** I'm not telling you to split into separate _shops_ β€” that's microservices, and you're not there yet. I'm telling you to organize by responsibility _within_ the same shop β€” a **modular monolith**. Each department is clearly boundaried, with its own space, its own records β€” so that _tomorrow_ , if one department genuinely outgrows the rest, you can lift it out into its own shop without tearing anything else apart.\n\n\n\n    shop/\n    β”œβ”€β”€ payments/       <- own logic, own records\n    β”œβ”€β”€ users/          <- own logic, own records\n    β”œβ”€β”€ vegetables/     <- own logic, own records\n    β”œβ”€β”€ masalas/        <- own logic, own records\n    └── delivery/       <- own logic, own records\n\n    (One shop. One deployable. But each room is self-contained β€”\n     nothing reaches into another room's private records directly.)\n\n\nπŸ‘¦ **Nephew:** Okay β€” my folders look like this now. Every department is separate and isolated, and there's one single front door β€” one entry point β€” for the whole shop. I've also turned on logging everywhere, so I can see which department is running slow and which is running fine.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** _(smiles)_ Now you're thinking like an engineer, not just a shopkeeper.\n\n\n\n    🧍 Customer\n        |\n        v\n    πŸšͺ ONE Front Door (single entry point / API gateway)\n        |\n        +--> payments/     [log: avg 45ms  OK]\n        +--> users/        [log: avg 20ms  OK]\n        +--> vegetables/   [log: avg 310ms SLOW!]\n        +--> masalas/      [log: avg 30ms  OK]\n        +--> delivery/     [log: avg 25ms  OK]\n\n\n##  Part 14: No More Standing in Line β€” Home Delivery\n\nπŸ‘¦ **Nephew:** I keep thinking β€” why should people stand in a queue at all? Let's do home delivery. That way the customer gets a fast acknowledgment, goes about their day, and we let them know once the order is packed and ready.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's the right instinct β€” it keeps your request/response fast and clean, and pushes the slow part out of the customer's way. How were you thinking of building it?\n\nπŸ‘¦ **Nephew:** That's what I wanted to ask you.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** There are two different tools for this, and picking the wrong one is one of the most common mistakes engineers make. Let's separate them clearly.\n\n###  Job Queue (BullMQ / Redis) β€” one task, one worker\n\nOrder comes in β†’ gets added to a queue β†’ **one** worker picks it up, packs the items, marks it done.\n\n\n\n    // Producer β€” order received, drop it in the queue, respond immediately\n    await orderQueue.add('pack-order', { orderId, items });\n    res.status(202).json({ message: 'Order received, packing now' });\n\n    // Consumer β€” a single worker fleet processes packing jobs one at a time\n    new Worker('pack-order', async job => {\n      await packItems(job.data.items);\n      await markOrderReady(job.data.orderId);\n    });\n\n\n###  Message Queue / Pub-Sub (RabbitMQ) β€” one event, many independent departments react\n\nOrder placed β†’ the news gets broadcast β†’ **every department that cares** reacts independently, without knowing about each other.\n\n\n\n                        \"order.placed\" event\n                                |\n            +-------------------+-------------------+\n            v                   v                   v\n      Payment dept         Inventory dept       Notification dept\n      makes invoice,        LOCKS the item      sends confirmation\n      sends payment link    (so 2 people can't    to customer\n                              book the same\n                              last item)\n\n\n\n    // Publisher β€” order service doesn't know or care who's listening\n    await eventBus.publish('order.placed', { orderId, userId, items });\n\n    // Payment department β€” reacts independently\n    eventBus.subscribe('order.placed', 'payment-service', async (event) => {\n      const invoice = await createInvoice(event);\n      await sendPaymentLink(event.userId, invoice);\n    });\n\n    // Inventory department β€” locks stock so two customers can't book the same last item\n    eventBus.subscribe('order.placed', 'inventory-service', async (event) => {\n      const locked = await inventory.lockItem(event.items, event.orderId);\n      if (!locked) {\n        await eventBus.publish('order.failed', { orderId: event.orderId, reason: 'out of stock' });\n      }\n    });\n\n\n###  The Locking Problem β€” Two People, One Last Tomato Crate\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That \"lock\" line matters more than it looks. Imagine two customers order the _last_ crate of tomatoes at almost the same second. Without a lock, both orders can succeed β€” and now you have to call one of them and say \"sorry, we'll refund you.\" Embarrassing, and avoidable.\n\n\n\n    // A row-level lock β€” only one request can hold this item at a time\n    async function lockItem(itemId, orderId) {\n      return db.transaction(async (trx) => {\n        const item = await trx('inventory')\n          .where({ id: itemId, status: 'available' })\n          .forUpdate() // locks this row until the transaction ends\n          .first();\n\n        if (!item) return false; // already taken by someone else\n\n        await trx('inventory')\n          .where({ id: itemId })\n          .update({ status: 'reserved', reservedBy: orderId });\n\n        return true;\n      });\n    }\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Once payment actually succeeds, the database transaction completes, and the payment department publishes one more event β€” `payment.successful`. From there, delivery-booking and inventory-management each pick it up and do their own independent work β€” nobody's waiting on anybody else.\n\n\n\n    Payment succeeds\n            |\n            v\n    \"payment.successful\" event published\n            |\n       +----+-----+\n       v          v\n    Delivery    Inventory\n    booking     finalizes stock\n    starts      deduction\n    (independent) (independent)\n\n\nπŸ‘¦ **Nephew:** So β€” job queue when exactly one worker should do a task. Message queue when many departments need to hear the same news and each decide what to do about it.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's the whole difference. Get that right, and half your \"distributed systems\" confusion disappears on its own.\n\n##  Part 15: The Race Condition β€” Two Hands, One Last Tomato\n\n_(A few days later, Nephew calls in a panic.)_\n\nπŸ‘¦ **Nephew:** Uncle, we have an emergency β€” we had to stop the shop for a while to find the root cause. There's a real mess in the system.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Take a breath. Now tell me clearly β€” what exactly happened?\n\nπŸ‘¦ **Nephew:** We had duplicate orders delivered, and sometimes we accepted an order but the item wasn't actually in inventory anymore. Customers got frustrated, complaints came in. I _did_ try to check \"if item is available, only then accept\" β€” but it still happened. First order got the item, but somehow the second order also got placed for the same item.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** In our world, that's called a **race condition**. Let me explain exactly what it means.\n\nImagine only _one_ tomato crate is left. Two customers place an order at almost the same instant.\n\n\n\n    Time -->\n\n    Customer A                          Customer B\n        |                                    |\n        |--Check: \"tomato available?\"------->|\n        |<--Yes, 1 left-----------------------|--Check: \"tomato available?\"-->|\n        |                                     |<--Yes, 1 left (STALE!)--------|\n        |--Accept order, deduct stock------->|\n        |                                     |--Accept order, deduct stock-->|\n        |                                     |\n        v                                     v\n      ORDER A: Accepted                   ORDER B: Accepted\n                  (BUT ONLY 1 TOMATO EXISTS -- you just sold it twice)\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** See the problem? Both checked \"is it available\" _before_ either one had actually finished acting on it. Both saw \"yes.\" Both proceeded. Neither knew about the other. That gap β€” between _checking_ and _acting_ β€” is exactly where a race condition lives. It's not a logic bug in your `if` statement. Your `if` statement is fine, checked in isolation. The bug only exists when two customers arrive close enough in time that their checks overlap.\n\n\n\n    // This LOOKS correct, but has a race condition:\n    async function placeOrder(itemId, orderId) {\n      const item = await db.inventory.findOne({ id: itemId });   // CHECK\n      if (item.stock > 0) {                                       // gap here!\n        await db.inventory.update(itemId, { stock: item.stock - 1 }); // ACT\n        return { success: true };\n      }\n      return { success: false, reason: 'out of stock' };\n    }\n    // Between the CHECK and the ACT, another request can slip in\n    // and read the exact same \"stock > 0\" state β€” before either write lands.\n\n\n###  The Fix: A Database Transaction With a Lock\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Databases have a very beautiful concept built exactly for this β€” it guarantees no two people can book the same last item, even if they arrive at the exact same nanosecond. The trick is to make \"check\" and \"act\" into a single, indivisible step β€” nobody else can peek in the middle.\n\n\n\n    // Correct version β€” check and deduct happen as ONE atomic operation\n    async function placeOrderSafely(itemId, orderId) {\n      const result = await db.inventory.updateOne(\n        { id: itemId, stock: { $gt: 0 } },   // condition checked AT THE MOMENT of update\n        { $inc: { stock: -1 } }              // deduct, only if condition still holds\n      );\n\n      if (result.modifiedCount === 0) {\n        return { success: false, reason: 'out of stock' };\n      }\n      return { success: true };\n    }\n\n\n\n    Customer A                          Customer B\n        |                                    |\n        |--UPDATE ... WHERE stock > 0------->|\n        |   (locks the row, deducts,         |\n        |    commits β€” all in one step)      |\n        |<--Success, stock now 0--------------|\n        |                                     |--UPDATE ... WHERE stock > 0-->|\n        |                                     |   (row now has stock = 0,     |\n        |                                     |    condition fails)           |\n        |                                     |<--0 rows modified-------------|\n        v                                     v\n      ORDER A: Accepted                   ORDER B: Rejected, cleanly\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** No separate \"check\" step to race against. The database itself checks the condition and applies the change as one uninterruptible unit β€” that's what a **transaction** with a proper `WHERE` guard gives you. Whoever's update reaches the row first wins; the second one simply sees the condition has already failed and fails cleanly, instead of both succeeding.\n\n###  Now the Second Problem β€” Duplicate Delivery\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Your second issue is different, and it needs a different kind of detective work. Go check your logs β€” logs are your graveyard for mistakes, every trace of what happened is sitting right there. Find out exactly what occurred:\n\n  * Did the customer place the order twice, but only _one_ payment actually went through?\n  * Did the duplication happen _after_ payment β€” like the delivery step ran twice for one single order?\n\n\n\nπŸ‘¦ **Nephew:** Let me go check the logs properly and trace it end to end.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Do that first. Never guess at a fix before you know exactly which step duplicated β€” the fix for \"duplicate order created\" and the fix for \"one order delivered twice\" are not the same thing at all.\n\n##  Part 16: What the Logs Revealed β€” The Retry Nobody Told You About\n\n_(Two days later, Nephew calls back.)_\n\nπŸ‘¦ **Nephew:** Uncle, I traced it like you said. Here's what I found β€” and it's strange. There's only **one payment**. Just one. But delivery ran **twice** , for the same order.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Good β€” that's real progress, because now we know exactly where the duplication is, instead of guessing across the whole shop. Walk me through the order of events, step by step, as the logs show it.\n\nπŸ‘¦ **Nephew:** Customer placed the order. Our app sent the request to the delivery department β€” \"please book delivery for order #812.\" But that request took a little too long to respond β€” maybe the delivery service was momentarily slow. The customer's app didn't get a response in time, and it automatically retried the same request.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** And the delivery department β€” did it know this second request was a _retry_ , or did it treat it as brand new?\n\nπŸ‘¦ **Nephew:** ...brand new. It has no way to know. It just saw \"book delivery for order #812\" arrive twice, and booked it twice.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** There's your whole mystery, solved. This isn't a race condition like the tomato problem β€” nobody raced anybody here. This is a much simpler, much more common issue: **the network doesn't guarantee you an answer, only an attempt.** A request can succeed on the delivery department's end, but the _response_ can get lost on the way back β€” slow connection, timeout, a phone network hiccup. From the customer's side, it looks like nothing happened, so it retries. From the delivery department's side, it looks like two completely separate, valid requests.\n\n\n\n    Customer's App                          Delivery Department\n          |                                        |\n          |--\"Book delivery, order #812\"---------->|\n          |                                        |--books it (SUCCESS)\n          |          X  (response lost/slow,       |\n          |              times out on app side)    |\n          |                                        |\n          |--(auto-retry) \"Book delivery,          |\n          |    order #812\"------------------------>|\n          |                                        |--books it AGAIN (thinks it's new!)\n          |<--Confirmed----------------------------|\n          |\n       Customer sees ONE confirmation.\n       Delivery department has booked it TWICE.\n\n\nπŸ‘¦ **Nephew:** So the retry itself isn't the mistake β€” retrying on a timeout is the _right_ thing for the customer's app to do.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Exactly right. You never want to punish the app for retrying β€” networks are unreliable, that's just reality. The mistake is that your delivery department has **no memory** of \"I already did this exact thing once.\" It needs a way to recognize: _this is the same request as before, not a new one._\n\n###  The Fix: An Idempotency Key β€” Like a Post Office Receipt\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Remember the post office we talked about long ago? A money order gets a receipt number the moment it's submitted. If the same money order gets submitted twice β€” because the sender's hand shook, or the counter clerk didn't hear \"confirmed\" the first time β€” a good post office never processes it twice just because it was _asked_ twice. It checks the receipt number first.\n\nThat's exactly what an **idempotency key** does. Every order gets a unique key generated _once_ , on the customer's side, before the first attempt. Every retry carries the _same_ key. The delivery department's job changes from \"do this thing\" to \"have I already done the thing with this key? If yes, just return the same result β€” don't do it again.\"\n\n\n\n    // Customer's app β€” generate the key ONCE, reuse it on every retry\n    const idempotencyKey = `order-812-delivery-${uuid()}`; // generated once, stored locally\n\n    async function bookDeliveryWithRetry() {\n      return fetch('/delivery/book', {\n        method: 'POST',\n        headers: { 'Idempotency-Key': idempotencyKey }, // same key on every retry\n        body: JSON.stringify({ orderId: 812 }),\n      });\n    }\n\n\n\n    // Delivery department β€” checks the key BEFORE doing any real work\n    app.post('/delivery/book', async (req, res) => {\n      const key = req.headers['idempotency-key'];\n\n      const existing = await db.idempotencyKeys.findOne({ key });\n      if (existing) {\n        // Already handled this exact request before β€” return the SAME result,\n        // don't book a second delivery\n        return res.status(200).json(existing.response);\n      }\n\n      const delivery = await createDeliveryBooking(req.body.orderId);\n\n      await db.idempotencyKeys.insertOne({\n        key,\n        response: delivery,\n        createdAt: new Date(),\n      });\n\n      return res.status(201).json(delivery);\n    });\n\n\n\n    Customer's App                          Delivery Department\n          |                                        |\n          |--Book, key=order-812-abc--------------->|\n          |                                        |--Not seen this key before\n          |                                        |--Books it, SAVES the key + result\n          |         X  (response lost)              |\n          |                                        |\n          |--(retry) Book, key=order-812-abc------->|\n          |                                        |--Seen this key already!\n          |                                        |--Returns SAME saved result,\n          |                                        |    does NOT book again\n          |<--Confirmed (same booking either way)---|\n\n\nπŸ‘¦ **Nephew:** So now, however many times the customer's app retries, the delivery department only ever _acts_ once β€” it just keeps handing back the same receipt.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Exactly. That's what \"idempotent\" means β€” doing the same operation once, or a hundred times, produces the _same_ end result, with no extra side effects. Once you build this into any operation that has a real-world consequence β€” booking, charging a card, sending a delivery truck β€” retries stop being dangerous, and start being what they were always meant to be: harmless safety nets.\n\n##  Part 17: The Telephone Exchange β€” Finding _Where_ the Delay Actually Was\n\nπŸ‘¦ **Nephew:** Fixed the duplicate booking. But uncle, honestly β€” it took me two full days of digging through separate logs from payment, inventory, and delivery, trying to line up timestamps by hand, just to figure out _where_ the slowness even started. There has to be a better way.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** There is, and this is exactly the right moment to learn it, because now you've _felt_ the pain it solves β€” that's the only way this lesson actually sticks. You remember the telephone exchange story? A crackling line, and the lineman walking pole to pole, checking every junction box, until he finds exactly where the signal died?\n\nπŸ‘¦ **Nephew:** Yes β€” you said that's what debugging a slow request across multiple services feels like without the right tool.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** You just lived it. One customer's order touched three separate departments β€” order intake, delivery booking, and whatever ran in between. Something in the middle was slow enough to cause a timeout. And instead of a report showing you exactly which hop took how long, you had three separate logbooks, three separate clocks, and you were manually trying to guess which line in the payment log happened \"around the same time\" as which line in the delivery log.\n\nπŸ‘¦ **Nephew:** Yes! Exactly that. Painful.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** This is what **distributed tracing** solves. The idea: the moment a customer's order enters your shop, it's handed a single **tracking thread** β€” a unique ID for that _one_ request's entire journey. Every department it passes through β€” order intake, payment, inventory, delivery β€” writes down how long _it_ took, but tags every single entry with that same tracking thread. At the end, instead of three separate logbooks you cross-reference by hand, you get one single report: one request, every hop it took, exactly how long each one held it.\n\n\n\n    One Customer Order β€” Tracking Thread: trace-812-xyz\n\n      Order Intake  --------> 5ms   OK\n      Payment       --------> 40ms  OK\n      Inventory Lock -------> 15ms  OK\n      Delivery Booking ------> 2100ms  <-- SLOW! (this is where the timeout came from)\n      ---------------------------------\n      Total: 2160ms\n\n\n\n    // Every department attaches the SAME trace ID as the request flows through\n    app.use((req, res, next) => {\n      req.traceId = req.headers['trace-id'] || generateTraceId();\n      res.setHeader('trace-id', req.traceId);\n      next();\n    });\n\n    // Delivery department logs its own timing, tagged with the shared trace ID\n    async function bookDelivery(req) {\n      const start = Date.now();\n      const result = await createDeliveryBooking(req.body.orderId);\n      logger.info({\n        traceId: req.traceId,\n        step: 'delivery-booking',\n        durationMs: Date.now() - start,\n      });\n      return result;\n    }\n\n    // When calling the NEXT department, pass the same trace ID forward\n    await fetch('http://inventory-service/lock', {\n      headers: { 'trace-id': req.traceId },\n    });\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** With this in place, the next time something's slow, you don't dig through three logbooks by hand for two days. You search for one trace ID, and you see the _entire_ journey of that one order, hop by hop, with exact timings β€” the way the lineman's route sheet showed exactly which pole failed, instead of him walking the whole street again from scratch.\n\nπŸ‘¦ **Nephew:** So if I'd had this the day the delivery bug happened...\n\nπŸ‘¨β€πŸ¦³ **Uncle:** You'd have searched one trace ID, seen \"delivery-booking: 2100ms, called twice\" in under a minute, and gone straight to the idempotency fix β€” instead of losing two days playing detective across three separate rooms. That's the entire value of tracing: it turns \"something, somewhere, was slow\" into \"this exact step, in this exact order, took this exact long.\"\n\n##  Part 18: The Diwali Rush β€” Load Balancers, and Doing the Math Before You Panic\n\n_(Weeks later, right before Diwali.)_\n\nπŸ‘¦ **Nephew:** Uncle, Diwali season is here and orders have shot up massively. One counter, one helper β€” I can't keep up. Some orders are getting missed, some customers wait forever, and honestly I feel stuck, like the whole shop is just... jammed.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** You've earned this problem, in a good way β€” it means the shop has reputation and consistency now, and people are showing up because of it. The fix is what we touched on early on, just at a bigger scale: put more counters behind a single \"traffic cop\" who decides which counter each customer goes to. In system terms β€” run multiple app instances behind a **load balancer**.\n\nπŸ‘¦ **Nephew:** But uncle, this load is only for the Diwali season. Once it's over, demand drops back to normal. Do I hire five new helpers permanently, and then fire them all a month later? That feels wasteful and messy.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** No β€” and this is exactly where most people panic and either overspend or under-prepare. Before you decide _how many_ counters you need, you calculate it. This is a method every engineer should know cold: **back-of-the-envelope estimation.** Rough numbers, done on paper in five minutes, that tell you roughly what you're dealing with β€” so you're not guessing, and you're not over-building either.\n\n###  Doing the Math β€” Normal Days vs Diwali\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Let's work through your actual shop's numbers together.\n\n**Step 1 β€” Normal day traffic:**\n\n\n\n    Normal day orders           : 500 orders/day\n    Shop active hours           : 12 hours/day\n    Total seconds in active day : 12 Γ— 3600 = 43,200 seconds\n\n    Average QPS (queries/orders per second) on a normal day:\n      500 orders Γ· 43,200 seconds β‰ˆ 0.012 orders/sec\n\n    That's basically nothing β€” one counter handles this easily.\n\n\n**Step 2 β€” Diwali day traffic:**\n\n\n\n    Diwali day orders           : 5,000 orders/day  (10x normal)\n    Same active hours           : 12 hours/day = 43,200 seconds\n\n    Average QPS during Diwali:\n      5,000 Γ· 43,200 β‰ˆ 0.116 orders/sec\n\n    Still sounds small on average β€” but averages LIE. Orders aren't\n    spread evenly across 12 hours. They spike hard in the evening.\n\n\n**Step 3 β€” Peak QPS is what actually matters, not the average:**\n\n\n\n    Assume 30% of the day's orders land in the busiest 2-hour window\n    (6 PM – 8 PM, when everyone's shopping for the evening puja/gifts):\n\n      Peak-window orders = 5,000 Γ— 0.30 = 1,500 orders\n      Peak-window seconds = 2 Γ— 3600 = 7,200 seconds\n\n      Peak QPS = 1,500 Γ· 7,200 β‰ˆ 0.21 orders/sec\n\n    Multiply in a safety buffer (real traffic is bursty, not smooth) β€”\n    engineers typically plan for 2-3x the calculated peak:\n\n      Design-for QPS β‰ˆ 0.21 Γ— 3 β‰ˆ 0.63 orders/sec, comfortably\n\n\n**Step 4 β€” How many counters (instances) do you actually need?**\n\n\n\n    Suppose ONE counter (one app instance) can reliably handle\n    0.15 orders/sec end-to-end (order intake + inventory check + payment).\n\n      Instances needed = Design-for QPS Γ· per-instance capacity\n                        = 0.63 Γ· 0.15\n                        β‰ˆ 4.2 β†’ round up β†’ 5 instances during peak Diwali hours\n\n\n**Step 5 β€” Storage, while you're at it:**\n\n\n\n    Each order record (order details, item list, payment ref) β‰ˆ 2 KB\n\n    Diwali month extra orders β‰ˆ 5,000/day Γ— 20 heavy days = 100,000 orders\n    Extra storage needed = 100,000 Γ— 2 KB β‰ˆ 200 MB\n\n    Negligible for a database β€” this is NOT your bottleneck.\n    Your bottleneck is concurrent REQUESTS, not stored DATA.\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** See what just happened? Five minutes of arithmetic told you: you don't need to hire and fire five permanent helpers. You need **5 counters active only during the Diwali peak window** , and storage was never even a real concern. This exact exercise β€” QPS, peak QPS with a buffer, capacity per unit, then instances needed β€” is what engineers do before touching any infrastructure decision. Guessing \"we'll need a lot more servers\" costs money. Calculating costs five minutes.\n\n###  Two Different Kinds of Load Balancer\n\nπŸ‘¦ **Nephew:** Okay, so I add more counters. But you called it a \"load balancer\" β€” is that one thing, or different things?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Good catch β€” there are two distinct levels, and people mix them up constantly.\n\n**1. Application-level load balancer β€” inside your own shop**\n\nThis is the traffic cop standing right at your shop's entrance, deciding which of your _own_ counters (app instances) gets the next customer. This is what tools like **Nginx** or a cloud **Application Load Balancer (ALB)** do β€” they sit in front of multiple copies of the _same_ app and spread requests across them.\n\n\n\n                    🧍 Customers\n                         |\n                         v\n             🚦 Load Balancer (Nginx / ALB)\n               /        |        |        \\\n              v         v        v         v\n         Counter 1  Counter 2  Counter 3  Counter 4\n         (instance) (instance) (instance) (instance)\n\n\n**2. Infrastructure-level / geo load balancer β€” across locations**\n\nThis is a completely different problem: not \"which counter in THIS shop,\" but \"which SHOP, in which city, should even receive this customer's call.\" Think of how a telecom company routes calls β€” if a call comes from the north side of the city, it's routed to the north exchange, not dragged across town to the south one. That's routing based on _where the request is coming from_ , to the _nearest_ full copy of your infrastructure.\n\n\n\n            🧍 Customer in Bengaluru      🧍 Customer in Delhi\n                    |                            |\n                    v                            v\n         🌐 Geo/DNS-based Router (routes by customer's location)\n                    |                            |\n                    v                            v\n         🏬 Bengaluru Shop (full copy)    🏬 Delhi Shop (full copy)\n         (its own counters + storeroom)   (its own counters + storeroom)\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** The first kind balances load _within_ one location. The second kind decides _which location_ should even be involved, usually to cut down travel distance (latency) and to survive one location going down entirely. You don't need the second one yet β€” that's a Metropolis-stage problem, not a Diwali-week problem. What you need right now is the first kind: more counters, same shop.\n\n###  Scaling the Counters Without Hiring and Firing People Manually\n\nπŸ‘¦ **Nephew:** So during Diwali, I run 5 counters. After Diwali, back to 1. How do I avoid doing that by hand every year?\n\nπŸ‘¨β€πŸ¦³ **Uncle:** You don't do it by hand at all β€” you set a rule, and let the system act on it. This is **autoscaling** , and there are two common triggers:\n\n  * **Scheduled scaling** β€” you already _know_ Diwali is coming. You tell the system: \"scale up to 5 instances starting Nov 8th, scale back down to 1 by Nov 20th.\" No guessing, no manual work, because the event is predictable.\n  * **Metric-based scaling** β€” for surges you _can't_ predict in advance, the system watches a signal (CPU usage, request queue depth, orders-per-second) and adds counters automatically when it crosses a threshold, then removes them again once things calm down.\n\n\n\n\n    // Scheduled autoscaling β€” Diwali is a known, recurring event\n    autoscaler.schedule({\n      name: 'diwali-2026-surge',\n      startDate: '2026-11-05',\n      endDate: '2026-11-20',\n      minInstances: 5,\n      maxInstances: 8,\n    });\n\n    // Metric-based autoscaling β€” for unpredictable spikes any other time\n    autoscaler.setPolicy({\n      metric: 'requests_per_second',\n      scaleUpThreshold: 0.5,   // add an instance if load crosses this\n      scaleDownThreshold: 0.1, // remove an instance once load drops below this\n      minInstances: 1,\n      maxInstances: 8,\n      cooldownSeconds: 300,    // wait before scaling again, avoid flapping\n    });\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** This way, come next Diwali, or any random viral day you didn't expect, the shop grows and shrinks itself based on real signals β€” not on you remembering to call five people the week before.\n\n###  The New Problem β€” More Counters, Inconsistent Answers\n\nπŸ‘¦ **Nephew:** Uncle, one more issue this week. Customers are calling to ask \"is this item available,\" and different counters are giving different answers β€” one counter says yes, another says no, for the exact same item. It's making us look disorganized, and it's slowing everyone down while we double-check.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Now you've hit the real cost of having multiple counters: they all need to agree on the _same_ truth, at the _same_ moment, and that agreement isn't free. Let's separate what you're actually offering into two different categories, because they don't need the same treatment.\n\n\n\n    CAN be slightly stale, safely cached, shown from any counter:\n      - Product catalog (what items you sell, descriptions, prices)\n      - Images, categories, \"popular items\" listings\n      - Anything that changes rarely and being 30 seconds old costs nothing\n\n    MUST be checked live, from the one source of truth, every single time:\n      - Exact stock count for a specific item (\"is there 1 tomato crate left\")\n      - Whether a specific order can still be placed\n      - Anything where being even a few seconds stale causes a real problem\n        (the exact bug you already fixed with atomic updates in Part 15)\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** So: catalog and descriptions β€” cache them, serve them from any counter, even push them close to the customer the way we discussed with the supermarket-in-every-neighborhood idea. But the actual **stock count** for a specific item β€” that question always goes straight to the one real storeroom record, the same atomic check-and-deduct you already built. Never let a counter answer a stock question from its own local guess.\n\nπŸ‘¦ **Nephew:** So it's not that caching is wrong here either β€” it's that I was caching the _wrong_ kind of information.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Exactly the same lesson as the milk and bread, from a few episodes ago, just wearing a bigger costume. Some answers can be a little old. Some answers can never be allowed to lie, even for a second.\n\n##  Part 19: Rate Limiting and Circuit Breakers β€” Protecting the Shop From Itself and From Others\n\n_(A week later.)_\n\nπŸ‘¦ **Nephew:** Uncle, two new problems this week. First β€” some customers are abusing our delivery service. One person ordered a _single_ milk packet, then called ten separate times for the same tiny order. It feels like they're misusing our service β€” like that line from the movie, \"meri shakti ka galat istemaal ho raha hai.\" A delivery guy's entire trip earns us maybe β‚Ή20 β€” this is a genuine waste of resources.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** I understand completely, and this is a very real problem, not a small annoyance. If your delivery capacity keeps getting eaten up by a handful of tiny, repeated β‚Ή20 orders, your genuine bigger-ticket customers end up waiting longer, or getting blocked out entirely. In our world, this is solved with **rate limiting** β€” you put a cap on how often, and how small, someone can order.\n\n###  Rate Limiting β€” Two Different Rules, Working Together\n\nπŸ‘¨β€πŸ¦³ **Uncle:** You mentioned it yourself, almost correctly β€” you'd want at least two kinds of limits working together:\n\n  1. **A frequency limit** β€” how many orders one customer can place in a given time window (say, 5–6 calls per hour).\n  2. **A minimum order size** β€” below a certain order value (say, β‚Ή120), delivery just isn't offered; they can still buy it, but they pick it up themselves, or it gets bundled with their next order.\n\n\n\n\n    // Rule 1 β€” Frequency limit: max 6 orders per customer, per hour\n    const rateLimiter = new Map(); // in real systems, this lives in Redis\n\n    async function checkFrequencyLimit(customerId) {\n      const key = `rate:${customerId}`;\n      const windowSeconds = 3600; // 1 hour\n\n      const count = await redis.incr(key);\n      if (count === 1) {\n        await redis.expire(key, windowSeconds); // window starts now\n      }\n\n      if (count > 6) {\n        throw new Error('Too many orders this hour. Please wait before ordering again.');\n      }\n    }\n\n    // Rule 2 β€” Minimum order value for delivery\n    function checkMinimumOrderValue(orderTotal) {\n      const MIN_DELIVERY_VALUE = 120;\n      if (orderTotal < MIN_DELIVERY_VALUE) {\n        throw new Error(`Orders under β‚Ή${MIN_DELIVERY_VALUE} aren't eligible for delivery β€” please add more items, or visit the counter.`);\n      }\n    }\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Notice the shape of this: the frequency limit uses a counter that resets every hour β€” that's the classic **fixed window** rate-limiting approach, simple and good enough for most shops. Real systems sometimes use a **sliding window** or **token bucket** instead, to avoid the awkward edge case where someone slips in 6 orders right at 11:59 and another 6 right at 12:01 β€” technically within two separate windows, but really 12 orders in two minutes. For your shop's scale, the simple fixed window is completely fine; don't over-engineer this on day one.\n\n\n\n    Fixed window (simple):\n      |--- Hour 1: max 6 orders ---|--- Hour 2: max 6 orders ---|\n            (resets sharply at the boundary β€” small edge-case risk)\n\n    Sliding window (more accurate, more complex):\n      Always looks back exactly 60 minutes from RIGHT NOW,\n      no matter what the clock says β€” closes the edge-case gap.\n\n\nπŸ‘¦ **Nephew:** Good β€” that solves the abuse problem. Second thing: our payment gateway failed for a bit yesterday, and the whole ordering flow just froze. How do real systems handle this? We use Razorpay, but we also have Cashfree as a backup β€” how do developers actually manage a failure like that?\n\n###  Circuit Breakers β€” Stop Hammering a Broken Door\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Good question, and this is one of the most important patterns for keeping one broken part from taking down your entire shop. Imagine your payment counter β€” every single order goes through it. Now imagine Razorpay, the actual payment processor behind that counter, goes down for five minutes. What happens if you keep sending every single customer's payment request straight at it, one after another, hoping it comes back?\n\nπŸ‘¦ **Nephew:** Every single order just hangs there waiting, and the customer sees nothing happen. The whole shop looks frozen even though only the _payment_ part is actually broken.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** Exactly β€” and worse, if Razorpay is struggling and half-alive, every failed retry from you adds _more_ load onto something that's already drowning, delaying its recovery even further. A **circuit breaker** solves this by watching the failure rate and, past a certain point, refusing to even _try_ calling the broken service for a while β€” like an electrical circuit breaker tripping to protect the rest of the house from a short.\n\n\n\n                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\n          failures      β”‚             β”‚      timeout passes,\n          keep rising    β”‚   CLOSED    β”‚      try one test call\n          past threshold β”‚  (normal)   β”‚\n               β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n               v                ^\n        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚\n        β”‚             β”‚   test call succeeds\n        β”‚    OPEN     β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n        β”‚  (blocked)  β”‚\n        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n               β”‚\n               v  test call fails again\n        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\n        β”‚  HALF-OPEN  │──> back to OPEN, wait longer next time\n        β”‚ (cautious)  β”‚\n        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n\n\n  * **Closed** β€” normal operation. Every payment request goes straight to Razorpay, like usual.\n  * **Open** β€” Razorpay has failed too many times in a row. The breaker \"trips\" β€” stop sending it any requests at all for a set period, and immediately fall back to something else, or fail fast with a clear message.\n  * **Half-open** β€” after a cooldown, let _one_ test request through. If it succeeds, close the breaker and resume normal traffic. If it fails again, go back to open and wait longer.\n\n\n\n\n    class CircuitBreaker {\n      constructor(fn, { failureThreshold = 5, cooldownMs = 30000 } = {}) {\n        this.fn = fn;\n        this.failureThreshold = failureThreshold;\n        this.cooldownMs = cooldownMs;\n        this.failureCount = 0;\n        this.state = 'CLOSED';\n        this.nextAttemptAt = null;\n      }\n\n      async call(...args) {\n        if (this.state === 'OPEN') {\n          if (Date.now() < this.nextAttemptAt) {\n            throw new Error('Payment service unavailable right now β€” try again shortly');\n          }\n          this.state = 'HALF_OPEN'; // cooldown passed, allow one test call\n        }\n\n        try {\n          const result = await this.fn(...args);\n          this.failureCount = 0;\n          this.state = 'CLOSED'; // success β€” fully close the breaker\n          return result;\n        } catch (err) {\n          this.failureCount++;\n          if (this.failureCount >= this.failureThreshold) {\n            this.state = 'OPEN';\n            this.nextAttemptAt = Date.now() + this.cooldownMs;\n          }\n          throw err;\n        }\n      }\n    }\n\n\n###  Using It β€” With a Fallback Gateway\n\n\n    const razorpayBreaker = new CircuitBreaker(chargeWithRazorpay, {\n      failureThreshold: 5,\n      cooldownMs: 30000,\n    });\n\n    async function processPayment(order) {\n      try {\n        return await razorpayBreaker.call(order);\n      } catch (err) {\n        logger.warn({ event: 'razorpay_unavailable', orderId: order.id });\n        // Breaker is open, or the call itself failed β€” fall back to Cashfree\n        return await chargeWithCashfree(order);\n      }\n    }\n\n\nπŸ‘¨β€πŸ¦³ **Uncle:** So when Razorpay starts failing repeatedly, the breaker trips open β€” new payment attempts don't even bother knocking on Razorpay's door anymore, they go straight to Cashfree instead, and the customer barely notices anything happened. After the cooldown, one quiet test request checks if Razorpay has recovered, and only resumes normal traffic once it confirms that. You're not just \"trying and catching an error\" anymore β€” you're actively protecting both your customer's experience _and_ the struggling service from getting hammered while it's trying to recover.\n\nπŸ‘¦ **Nephew:** So rate limiting protects the shop from _too many_ legitimate-but-abusive requests, and the circuit breaker protects the shop from _one broken dependency_ dragging everything else down with it.\n\nπŸ‘¨β€πŸ¦³ **Uncle:** That's the whole distinction, in one sentence. One is about _volume_ you don't want. The other is about _failure_ you can't control β€” but absolutely can contain.\n\n_Next time: pushing the catalog closer to the customer, the supermarket-in-every-neighborhood idea β€” properly built._",
  "title": "The Shop on the Corner: How I Learned System Design Without Building Clone"
}