<?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: kirandeepjassal-crypto</title>
    <description>The latest articles on DEV Community by kirandeepjassal-crypto (@kirandeepjassalcrypto).</description>
    <link>https://dev.to/kirandeepjassalcrypto</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%2F3948965%2F92a8ebec-5c78-46dc-b19b-babd45c794b0.png</url>
      <title>DEV Community: kirandeepjassal-crypto</title>
      <link>https://dev.to/kirandeepjassalcrypto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kirandeepjassalcrypto"/>
    <language>en</language>
    <item>
      <title>Design a Notification System — Push, SMS &amp; Email at Scale (with Production .NET Code)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Sat, 05 Sep 2026 19:30:59 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/design-a-notification-system-push-sms-email-at-scale-with-production-net-code-49b5</link>
      <guid>https://dev.to/kirandeepjassalcrypto/design-a-notification-system-push-sms-email-at-scale-with-production-net-code-49b5</guid>
      <description>&lt;p&gt;"Design a notification system" looks like an integration task — call Twilio, call SendGrid, done. The interview lives in everything &lt;em&gt;around&lt;/em&gt; those calls: how do you fan one event out to a million recipients across three channels, make sure a marketing blast never delays someone's login OTP, avoid double-sending when your queue redelivers a message, and survive Twilio having a bad afternoon? It's a queues-and-reliability problem wearing an integration costume.&lt;/p&gt;

&lt;p&gt;This is the condensed walkthrough; the full guide (the pipeline, the four hard parts, delivery tracking, and the full production .NET 9 code) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/design-a-notification-system-system-design" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/design-a-notification-system-system-design&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The design at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core shape&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Enqueue → per-channel workers → providers&lt;/strong&gt; (decouple ingestion from delivery)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fan-out&lt;/td&gt;
&lt;td&gt;Expand recipients → one queued message per (user, channel)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Separate queues&lt;/strong&gt; — transactional never waits behind bulk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery guarantee&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;At-least-once + dedup&lt;/strong&gt; (exactly-once across third parties is a myth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failures&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Retry with backoff + jitter&lt;/strong&gt;, then &lt;strong&gt;dead-letter queue&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Politeness&lt;/td&gt;
&lt;td&gt;User preferences, quiet hours, &lt;strong&gt;per-user rate limiting&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why it's a queue problem, not an integration problem
&lt;/h2&gt;

&lt;p&gt;Estimate it: 100M notifications/day ≈ ~1,160/sec average — trivial. But a campaign blast to 50M users in minutes is &lt;em&gt;tens of thousands/sec&lt;/em&gt;. The &lt;strong&gt;spike&lt;/strong&gt; is the design driver, and a queue in the middle absorbs it so you never hand that burst straight to a rate-limited provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Event sources (services, campaigns)
       │
       ▼
 [ Notification Service ]  — validate · apply prefs · dedup · render template
       │
       ▼
 [ Message Queue ]                    (priority: transactional vs bulk)
    │              │              │
    ▼              ▼              ▼
[Push worker]  [SMS worker]  [Email worker]     — scale independently
    │              │              │
    ▼              ▼              ▼
  APNs / FCM     Twilio          SES            — third-party providers
    │              │              │
    └──── delivery receipts (webhooks) ────▶ update status · DLQ on repeated failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The four hard parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Fan-out.&lt;/strong&gt; One event can target millions. Expand the recipient list and enqueue &lt;strong&gt;one message per (user, channel)&lt;/strong&gt; — in &lt;strong&gt;batches&lt;/strong&gt; for huge audiences, so one request doesn't block producing millions of messages. Fan-out is where a tiny API call becomes a tidal wave; the queue is the seawall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Priority — the OTP must not wait.&lt;/strong&gt; A marketing blast enqueues tens of millions of messages. If a login OTP lands behind them in the same FIFO queue, it's useless by the time it arrives. &lt;strong&gt;Separate queues by priority&lt;/strong&gt; — a high-priority transactional queue with its own workers, and a bulk queue for campaigns. Never let them share a lane.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deduplication.&lt;/strong&gt; Queues are &lt;strong&gt;at-least-once&lt;/strong&gt;: a worker crash after sending but before acking causes redelivery. Guard every send with an &lt;strong&gt;idempotency key&lt;/strong&gt; (&lt;code&gt;eventId + userId + channel&lt;/code&gt;) checked against Redis before dispatch. If it's already marked sent, skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Retries + dead-letter queue.&lt;/strong&gt; Providers fail (timeouts, 500s, throttling). Retry with &lt;strong&gt;exponential backoff + jitter&lt;/strong&gt; up to a max, then send to a &lt;strong&gt;dead-letter queue&lt;/strong&gt; for inspection rather than losing it or retrying forever. Wrap each provider in a &lt;strong&gt;circuit breaker&lt;/strong&gt;, and ideally fail over to a &lt;strong&gt;backup provider&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Sent" ≠ "delivered" ≠ "read"
&lt;/h2&gt;

&lt;p&gt;Handing a message to Twilio isn't the phone buzzing. Consume &lt;strong&gt;delivery-receipt webhooks&lt;/strong&gt; to move &lt;code&gt;status&lt;/code&gt; from &lt;code&gt;sent&lt;/code&gt; → &lt;code&gt;delivered&lt;/code&gt; (or &lt;code&gt;bounced&lt;/code&gt;/&lt;code&gt;failed&lt;/code&gt;), and suppress future sends to a hard-bounced address. Apply user &lt;strong&gt;preferences and quiet hours&lt;/strong&gt; &lt;em&gt;before&lt;/em&gt; enqueueing, and &lt;strong&gt;cap frequency per user&lt;/strong&gt; (batch low-priority notifications into a digest). This is both compliance (TCPA/GDPR/CAN-SPAM) and anti-fatigue.&lt;/p&gt;

&lt;h2&gt;
  
  
  I shipped this in production
&lt;/h2&gt;

&lt;p&gt;We fan out ~15M events/day (&lt;code&gt;budget.threshold.crossed&lt;/code&gt;, &lt;code&gt;campaign.completed&lt;/code&gt;, &lt;code&gt;conversion.tracked&lt;/code&gt;) to customer webhooks, email, and push. V1 dispatched &lt;strong&gt;inline in the request&lt;/strong&gt; that produced the event — a deploy that recycled the app mid-flight dropped every in-memory delivery, and one slow customer endpoint dragged API latency up with it. Moving to &lt;strong&gt;outbox → Azure Service Bus → worker pool&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Events dropped per deploy&lt;/td&gt;
&lt;td&gt;Thousands&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0&lt;/strong&gt; (outbox + queue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First-attempt delivery success&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~96%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eventual delivery success&lt;/td&gt;
&lt;td&gt;Lost on first failure&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~99.98%&lt;/strong&gt; (backoff + retry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API write-path p95&lt;/td&gt;
&lt;td&gt;Coupled to slow endpoints&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;120ms&lt;/strong&gt; (decoupled)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failing-endpoint handling&lt;/td&gt;
&lt;td&gt;Retried inline, blocked workers&lt;/td&gt;
&lt;td&gt;Auto-disabled after 20 consecutive failures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The producer never blocks on a customer's endpoint — it only writes to the queue — and the worker owns the retry clock via scheduled messages, so a slow or dead endpoint can never back-pressure the API. A Redis &lt;code&gt;SET&lt;/code&gt;-based dedup key makes at-least-once safe to retry; the breaker sheds load from any endpoint that has failed 20 times in a row. (Full .NET 9 producer + worker + circuit breaker is in the post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The model to carry forward
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A notification system is a queue that buffers your events from unreliable third-party channels.&lt;/strong&gt; Everything hard about it — fan-out, priority, dedup, retries — is solved by &lt;em&gt;not&lt;/em&gt; calling providers inline. Three habits it teaches: put a queue in the middle (it answers bursts, outages, and retries at once); separate urgent from bulk (the OTP-behind-the-newsletter failure is the one interviewers probe for); assume redelivery, so dedup.&lt;/p&gt;

&lt;p&gt;The full guide has the full pipeline, all four hard parts in depth, delivery tracking + preferences, the design checklist, the complete production .NET 9 outbox→Service Bus worker (dedup, backoff+jitter, circuit breaker, DLQ), and the "when it's overkill" honest section:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/design-a-notification-system-system-design" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/design-a-notification-system-system-design&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/design-a-notification-system-system-design" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>interview</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Design a Distributed Cache — Consistent Hashing, Eviction &amp; Replication (with Production .NET Code)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Fri, 04 Sep 2026 08:13:59 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/design-a-distributed-cache-consistent-hashing-eviction-replication-with-production-net-code-3bli</link>
      <guid>https://dev.to/kirandeepjassalcrypto/design-a-distributed-cache-consistent-hashing-eviction-replication-with-production-net-code-3bli</guid>
      <description>&lt;p&gt;"Design a distributed cache" is really two interviews stacked on top of each other. First: build an in-memory cache on one machine — the classic O(1) LRU question. Then the twist that makes it &lt;em&gt;distributed&lt;/em&gt;: the hot data is bigger than one machine's RAM, so you spread it across a cluster — and now you have to answer how keys map to nodes without every key moving when a node joins, what happens when a node dies, and how the cache stays consistent with the database behind it.&lt;/p&gt;

&lt;p&gt;This is the condensed walkthrough; the full guide (the O(1) LRU structure, all the write policies, the hard parts, and the full production .NET 9 code) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/design-a-distributed-cache-system-design" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/design-a-distributed-cache-system-design&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The design at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single-node cache&lt;/td&gt;
&lt;td&gt;Hashmap + doubly-linked list → &lt;strong&gt;O(1) LRU&lt;/strong&gt; get/put&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sharding&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Consistent hashing&lt;/strong&gt; with &lt;strong&gt;virtual nodes&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eviction&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;LRU&lt;/strong&gt; default; &lt;strong&gt;LFU&lt;/strong&gt; for skewed popularity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Replication&lt;/strong&gt; (primary + replicas), promote on failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache ↔ DB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Cache-aside&lt;/strong&gt; by default; write-through / write-back / write-around as needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistency&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Eventually consistent&lt;/strong&gt; — accept a small stale window&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The single-node cache: O(1) LRU
&lt;/h2&gt;

&lt;p&gt;Before distributing anything, build the one-machine cache with two structures: a &lt;strong&gt;hashmap&lt;/strong&gt; &lt;code&gt;key → node&lt;/code&gt; for O(1) lookup, and a &lt;strong&gt;doubly-linked list&lt;/strong&gt; ordered by recency (MRU at the head, LRU at the tail).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get(key):   look up in map; move its node to the head; return value
put(key,v): insert at head; if over capacity, evict the tail (LRU); update map
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moving to the head and evicting the tail are both O(1). This is the atom every cache node is built from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core of the interview: consistent hashing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;hash(key) % N&lt;/code&gt; fails.&lt;/strong&gt; It works until &lt;code&gt;N&lt;/code&gt; changes. Add one node (N → N+1) and the modulus changes for &lt;strong&gt;almost every key&lt;/strong&gt;, so nearly the entire cache remaps — a mass of misses that all fall through to the DB at once. Adding capacity shouldn't nuke your cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistent hashing.&lt;/strong&gt; Map both nodes and keys onto a ring (&lt;code&gt;0 … 2^32&lt;/code&gt;). A key is owned by the &lt;strong&gt;first node clockwise&lt;/strong&gt; from its hash.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 ──── A ──── key1 ──── B ──── key2 ──── C ──── key3 ──(wraps)── 0
                └▶ B          └▶ C          └▶ A (wraps around)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add node D between B and C and only the keys in arc &lt;code&gt;(B … D]&lt;/code&gt; move — roughly &lt;strong&gt;1/N of the keys&lt;/strong&gt;. Everything else stays put. ~1/N churn instead of ~everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual nodes.&lt;/strong&gt; With only N points on the ring, placement is uneven and one node can own a giant arc (a hotspot). Place each physical node at &lt;strong&gt;many&lt;/strong&gt; points (100–200 vnodes) so load averages out, nodes can be weighted, and a departing node's load spreads evenly instead of dumping on one neighbor.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LRU (least recently used)&lt;/strong&gt; — the default; great for temporal locality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LFU (least frequently used)&lt;/strong&gt; — better when a stable set of keys is persistently hot and you don't want a one-off scan to flush them.&lt;/li&gt;
&lt;li&gt;TTL expiry runs alongside whichever policy you pick.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Write policies (where cache correctness lives)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache-aside (lazy loading) — the default.&lt;/strong&gt; Read cache; on a miss read the DB and populate. On a write, update the DB and &lt;strong&gt;invalidate&lt;/strong&gt; the key. Simple and resilient; the trade is a stale window + a miss-penalty on cold keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-through&lt;/strong&gt; — write cache &lt;strong&gt;and&lt;/strong&gt; DB synchronously. Always fresh, higher write latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-back&lt;/strong&gt; — write cache, flush to DB async. Fast writes, but a crash before flush &lt;strong&gt;loses data&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-around&lt;/strong&gt; — write straight to the DB, skip the cache; fills on later reads. Good for write-once-read-rarely data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The genuinely hard parts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache invalidation&lt;/strong&gt; — TTL is the pragmatic default; delete-on-write is tighter; event-driven is tightest and most complex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thundering herd (cache stampede)&lt;/strong&gt; — a hot key expires and thousands of misses hammer the DB at once. Fixes: &lt;strong&gt;single-flight/locking&lt;/strong&gt;, &lt;strong&gt;jittered TTLs&lt;/strong&gt;, &lt;strong&gt;stale-while-revalidate&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hot keys&lt;/strong&gt; — one wildly popular key overwhelms its owning node; consistent hashing can't split a single key. Replicate the hot key to multiple nodes or cache it in the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Replication
&lt;/h2&gt;

&lt;p&gt;Each shard has a &lt;strong&gt;primary + replicas&lt;/strong&gt;. Replicas serve reads and stand ready for failover — when a primary dies, a replica is promoted and only that shard briefly degrades. Without replication, a dead node turns every key it held into a miss and spikes the DB.&lt;/p&gt;

&lt;h2&gt;
  
  
  I shipped this in production
&lt;/h2&gt;

&lt;p&gt;Our dashboard recomputes campaign KPIs by aggregating over a &lt;strong&gt;1.2B-row &lt;code&gt;CampaignEvents&lt;/code&gt; table&lt;/strong&gt; in Azure SQL. Every dashboard load fanned out into repeated full aggregate scans — the same tenant/campaign/date-range combos recomputed thousands of times a minute, p95 at 2,100ms, pinning the DB. A cache-aside layer (short TTL for freshness, version-token invalidation on ingestion for correctness):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard KPI query p95&lt;/td&gt;
&lt;td&gt;2,100 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database CPU&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working set&lt;/td&gt;
&lt;td&gt;2.1 GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;380 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL cost&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$280/mo saved&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Correctness comes from a &lt;strong&gt;per-campaign version token&lt;/strong&gt;, not the TTL: a Kafka ingestion event bumps the version, which instantly makes every cached date range for that campaign unreachable — no &lt;code&gt;SCAN&lt;/code&gt;/&lt;code&gt;DEL&lt;/code&gt; sweep at 3,200 req/sec. The key is &lt;code&gt;kpi:{tenant}:{campaign}:{range}:v{version}&lt;/code&gt;. (Full .NET 9 &lt;code&gt;DashboardKpiService&lt;/code&gt; is in the post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The model to carry forward
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A distributed cache is an O(1) LRU cache, sharded by consistent hashing, kept loosely in sync with a database.&lt;/strong&gt; The single-node part is a data-structures exercise; the distributed part is all about &lt;em&gt;change&lt;/em&gt; — making node joins/failures cheap (consistent hashing + vnodes) and the cache-DB gap manageable (write policies + invalidation). It rests on one permission: the cache is allowed to be a little wrong for a little while, and that's what buys the speed. Three habits: reach for consistent hashing the moment you shard, name the thundering herd, and pick a write policy on purpose.&lt;/p&gt;

&lt;p&gt;The full guide has the O(1) LRU walkthrough, consistent hashing + vnodes in depth, all four write policies, replication/failover, the thundering-herd/hot-key fixes, the design checklist, and the complete production .NET 9 cache-aside KPI service:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/design-a-distributed-cache-system-design" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/design-a-distributed-cache-system-design&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/design-a-distributed-cache-system-design" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>interview</category>
      <category>redis</category>
      <category>programming</category>
    </item>
    <item>
      <title>Design a Rate Limiter — Token Bucket, Sliding Window &amp; Distributed Limits (with Production .NET Code)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Wed, 02 Sep 2026 16:20:09 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/design-a-rate-limiter-token-bucket-sliding-window-distributed-limits-with-production-net-5lg</link>
      <guid>https://dev.to/kirandeepjassalcrypto/design-a-rate-limiter-token-bucket-sliding-window-distributed-limits-with-production-net-5lg</guid>
      <description>&lt;p&gt;"Design a rate limiter" sounds like a five-minute answer — count requests, block past the limit. Then the interviewer starts pulling threads: &lt;em&gt;which algorithm, and what's the burst behaviour at the window edge? Where does the counter live when you have 500 app servers? What happens to two requests that read the same counter at the same millisecond? What if the counter store goes down — block everyone or let everyone through?&lt;/em&gt; Those follow-ups are the whole interview.&lt;/p&gt;

&lt;p&gt;This is the condensed walkthrough; the full guide (all five algorithms, the distributed section, and the full production .NET 9 code) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/design-a-rate-limiter-system-design" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/design-a-rate-limiter-system-design&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The design at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Algorithm&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Token bucket&lt;/strong&gt; (bursts, O(1)) or &lt;strong&gt;sliding-window counter&lt;/strong&gt; (accurate, smooth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it lives&lt;/td&gt;
&lt;td&gt;API gateway / middleware, in front of app servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State store&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Redis&lt;/strong&gt; — shared across all servers so the limit is global&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atomicity&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Redis + a Lua script&lt;/strong&gt; (or &lt;code&gt;INCR&lt;/code&gt;) so concurrent requests can't over-count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On store failure&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Fail open&lt;/strong&gt; for general throttling; fail &lt;strong&gt;closed&lt;/strong&gt; for security limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;429&lt;/code&gt; + &lt;code&gt;Retry-After&lt;/code&gt; and &lt;code&gt;X-RateLimit-*&lt;/code&gt; headers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why it's inline-on-every-request
&lt;/h2&gt;

&lt;p&gt;A rate limiter runs on &lt;em&gt;every&lt;/em&gt; request, so its cost is paid by all traffic, all the time. That reframes every choice: the algorithm must be O(1), the store lookup atomic and sub-millisecond, and the failure mode must not take your whole API down. Say the API peaks at 1M req/sec — the limiter must sustain &lt;strong&gt;1M checks/sec at &amp;lt; 1 ms each&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counting algorithms (the core of the interview)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Fixed window counter.&lt;/strong&gt; &lt;code&gt;INCR&lt;/code&gt; a per-window key; reset each window. Dead simple, O(1) — but the &lt;strong&gt;boundary burst&lt;/strong&gt;: a client sends the full limit at &lt;code&gt;00:00:59&lt;/code&gt; and again at &lt;code&gt;00:01:00&lt;/code&gt; → &lt;strong&gt;2× the limit in ~1 second&lt;/strong&gt;. Know this trap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Sliding window log.&lt;/strong&gt; Store a timestamp for every request (Redis sorted set); drop old ones, count the rest. Perfectly accurate, no boundary burst — but memory grows with volume. Expensive for busy clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Sliding window counter (the sweet spot).&lt;/strong&gt; Approximate the rolling window with two fixed-window counts weighted by overlap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;estimated = current_count + previous_count × (overlap fraction)
# 30s into the current minute: prev_count × 0.5 + current_count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O(1) memory, smooths the boundary burst, close enough for almost everything. What many CDNs use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Token bucket (the industry default — Stripe, AWS).&lt;/strong&gt; A bucket holds up to &lt;strong&gt;C tokens&lt;/strong&gt;, refills at &lt;strong&gt;r tokens/sec&lt;/strong&gt;; each request takes one, empty = reject.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lastRefill&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# lazy refill
&lt;/span&gt;  &lt;span class="n"&gt;lastRefill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ALLOW&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DENY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stores only &lt;code&gt;{tokens, lastRefill}&lt;/code&gt; (O(1)), allows controlled bursts up to &lt;code&gt;C&lt;/code&gt; while holding the average at &lt;code&gt;r&lt;/code&gt;, refills lazily (no timer).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Leaky bucket.&lt;/strong&gt; FIFO queue processed at a constant rate; overflow rejected. Where token bucket &lt;em&gt;allows&lt;/em&gt; bursts, leaky bucket &lt;em&gt;smooths&lt;/em&gt; them into a steady stream — useful for shaping traffic to a rate-sensitive downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to pick:&lt;/strong&gt; token bucket for a general API limiter; sliding-window counter when you want accuracy without the log's memory. Fixed window only when the boundary burst genuinely doesn't matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it distributed (the second hard part)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local counters don't add up.&lt;/strong&gt; 500 servers each with their own bucket → client gets &lt;code&gt;500 × limit&lt;/code&gt;. Fix: a &lt;strong&gt;shared&lt;/strong&gt; Redis store, one authoritative count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The read-modify-write race.&lt;/strong&gt; Two requests hit two servers, both read &lt;code&gt;tokens=1&lt;/code&gt;, both allow, both decrement → two through a bucket that had one. &lt;strong&gt;Fix: atomicity&lt;/strong&gt; — &lt;code&gt;INCR&lt;/code&gt; for fixed window, a &lt;strong&gt;Redis Lua script&lt;/strong&gt; wrapping read-check-write for token bucket / sliding window (runs atomically, no interleaving).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Redis round trip on every request.&lt;/strong&gt; Co-locate + pipeline; or a hybrid local approximation reconciled periodically; or sticky routing (hash each client to a fixed server so its local bucket is authoritative).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis as a hot spot / SPOF.&lt;/strong&gt; Shard counters by &lt;code&gt;clientId&lt;/code&gt;, replicate for failover; shard a single extremely hot client's budget across keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fail open vs fail closed
&lt;/h2&gt;

&lt;p&gt;If Redis is unreachable, do you &lt;strong&gt;allow&lt;/strong&gt; everything (fail open — protects availability, risks overload) or &lt;strong&gt;block&lt;/strong&gt; everything (fail closed — protects the resource, risks an outage)? &lt;strong&gt;Default to fail open&lt;/strong&gt; for general throttling; fail &lt;strong&gt;closed&lt;/strong&gt; for security-critical limits like login or payment attempts. It's a one-sentence answer that shows judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  I shipped this in production
&lt;/h2&gt;

&lt;p&gt;Our public API peaks at ~3,200 req/sec across tenants and originally had no per-tenant ceiling — a single runaway scraper could push p99 into the seconds and drag every other tenant down. Moving a per-tenant token bucket to the gateway (atomic Redis Lua, 200 burst / 100 req/s):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API p95 latency&lt;/td&gt;
&lt;td&gt;480 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;120 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 during a single-tenant burst&lt;/td&gt;
&lt;td&gt;multi-second spikes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;flat, no cross-tenant spike&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blast radius of one abusive tenant&lt;/td&gt;
&lt;td&gt;all tenants degraded&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;isolated (429 + Retry-After)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-tenant budget&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;token bucket, 200 burst / 100 req/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The read-refill-check-decrement is a single atomic Redis Lua script, so concurrent gateway instances can never double-spend a tenant's budget, and the tenant id is read from the &lt;em&gt;verified token&lt;/em&gt; (never a caller-supplied argument) so one client can't spend down another's bucket. (Full .NET 9 middleware + Lua script is in the post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The model to carry forward
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A rate limiter is an algorithm choice wrapped in a distributed-consistency problem.&lt;/strong&gt; Three habits it teaches: (1) name the boundary burst; (2) make the shared update atomic, always — the read-modify-write race is the bug that quietly lets everyone past; (3) decide the failure mode on purpose.&lt;/p&gt;

&lt;p&gt;The full guide has all five algorithms in depth, the full distributed section, the design checklist, the complete production .NET 9 token-bucket middleware + Lua script, and the "when it's overkill" honest section:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/design-a-rate-limiter-system-design" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/design-a-rate-limiter-system-design&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/design-a-rate-limiter-system-design" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>interview</category>
      <category>redis</category>
      <category>programming</category>
    </item>
    <item>
      <title>Design a URL Shortener (bit.ly) — The Complete System Design Walkthrough (with Production .NET Code)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Tue, 01 Sep 2026 18:33:57 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/design-a-url-shortener-bitly-the-complete-system-design-walkthrough-with-production-net-code-3lg0</link>
      <guid>https://dev.to/kirandeepjassalcrypto/design-a-url-shortener-bitly-the-complete-system-design-walkthrough-with-production-net-code-3lg0</guid>
      <description>&lt;p&gt;"Design a URL shortener" is the question almost every system design interview opens with — and that's exactly why it's worth nailing. It looks trivial ("it's just a hashmap"), but the follow-ups are where candidates fall apart: how do you generate a short code with no collisions at a billion URLs? How do you serve 120,000 redirects a second without melting the database? 301 or 302?&lt;/p&gt;

&lt;p&gt;This is the condensed walkthrough; the full guide (every step, all three key-generation approaches, the Kafka analytics path, and the full production .NET 9 code) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/design-a-url-shortener-system-design" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/design-a-url-shortener-system-design&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The design at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Short code&lt;/td&gt;
&lt;td&gt;7-char &lt;strong&gt;base62&lt;/strong&gt; → ~3.5 trillion combinations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key generation&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Key Generation Service (KGS)&lt;/strong&gt; — pre-generate unique keys, hand them out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Key-value store (&lt;code&gt;shortCode → longUrl&lt;/code&gt;), sharded by &lt;code&gt;shortCode&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read path&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Cache-first&lt;/strong&gt; (Redis) — 100:1 read:write means the cache does the work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirect&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;301&lt;/strong&gt; for performance, &lt;strong&gt;302&lt;/strong&gt; if you need per-click analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale&lt;/td&gt;
&lt;td&gt;~1,200 writes/sec, ~120,000 redirects/sec, ~90 TB over 5 years&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Estimates decide everything
&lt;/h2&gt;

&lt;p&gt;100M new URLs/day, 100:1 read:write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Writes:  100,000,000 / 86,400 ≈ 1,160/sec   (~1.2k/sec — trivial)
Reads:   100x writes          ≈ 116,000/sec  (~120k/sec)
Peak (≈2–3x)                  ≈ ~300k/sec
Storage (5 yr): ~180B URLs × ~500 B ≈ ~90 TB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two takeaways: &lt;strong&gt;writes are trivial&lt;/strong&gt;, and &lt;strong&gt;~120k reads/sec is what forces caching and replication.&lt;/strong&gt; The redirect path must almost never touch disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 7 chars?&lt;/strong&gt; &lt;code&gt;62^6 ≈ 57B&lt;/code&gt; (too few for ~180B URLs); &lt;code&gt;62^7 ≈ 3.5 trillion&lt;/code&gt; (plenty). So 7 it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The heart of the problem: generating the short code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Approach A — hash the URL.&lt;/strong&gt; Truncate MD5/SHA-256 to 7 base62 chars. Problem: truncation &lt;em&gt;will&lt;/em&gt; collide, forcing a DB check + re-hash on every write (stateful, slow), and identical URLs collide to the same code. Avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach B — counter + base62.&lt;/strong&gt; Each URL gets the next integer, base62-encoded. Zero collisions by construction, but a single counter is a bottleneck + SPOF, and sequential codes are guessable. Fix the bottleneck by handing out &lt;em&gt;ranges&lt;/em&gt; (a coordinator gives each server a block of 10,000).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approach C — Key Generation Service (what to ship).&lt;/strong&gt; Pre-generate unique 7-char keys &lt;em&gt;offline&lt;/em&gt; into a pool split into unused/used. On a write, grab an unused key and mark it used — O(1), no hashing, no collision check on the request path. Each server checks out a &lt;em&gt;block&lt;/em&gt; into memory so it rarely hits the KGS. Run a standby replica; losing an in-memory block is harmless (3.5T keyspace).&lt;/p&gt;

&lt;h2&gt;
  
  
  The read path is cache-first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client → LB → stateless app servers
                 │
           [ Redis ] ── hit (~90%+) ──▶ 301/302
                 │ miss
                 ▼
           [ KV store ] ─▶ populate cache ─▶ redirect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Link popularity is heavily skewed, so an LRU Redis cache holding the hot links absorbs the vast majority of the 120k reads/sec. Shard the KV store by &lt;code&gt;shortCode&lt;/code&gt; — it's uniformly distributed, so no hot shards.&lt;/p&gt;

&lt;h2&gt;
  
  
  301 vs 302 — a real trade-off
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;301 (permanent)&lt;/strong&gt; lets the browser cache the redirect: subsequent clicks skip your server entirely (fast, cheap, but you &lt;em&gt;lose per-click analytics&lt;/em&gt;). &lt;strong&gt;302 (temporary)&lt;/strong&gt; routes every click through you: full analytics and a changeable target, at the cost of more load. Analytics-driven shorteners lean 302.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking clicks without slowing the redirect
&lt;/h2&gt;

&lt;p&gt;If you chose 302 and analytics matter, never write to an analytics DB &lt;em&gt;on&lt;/em&gt; the redirect path. Fire-and-forget onto a queue and return immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /{shortCode}
  → look up longUrl (cache)   ~1–2ms
  → emit click event to Kafka  fire-and-forget
  → 302 redirect               returned immediately
        [ Kafka ] → [ workers ] → [ analytics store ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  I shipped this in production
&lt;/h2&gt;

&lt;p&gt;Our report-sharing feature first embedded a 36-char sequential-GUID primary key in the share URL — long, ugly, enumerable, and a &lt;code&gt;SELECT&lt;/code&gt; against Azure SQL on every open. Rebuilt as exactly this design (KGS handing out 7-char base62 codes by the block, cache-first .NET 9 endpoint):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Share-link format&lt;/td&gt;
&lt;td&gt;36-char sequential GUID&lt;/td&gt;
&lt;td&gt;7-char base62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guessability&lt;/td&gt;
&lt;td&gt;Enumerable&lt;/td&gt;
&lt;td&gt;Crypto-random, ~3.5T keyspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirect data source&lt;/td&gt;
&lt;td&gt;Azure SQL every open&lt;/td&gt;
&lt;td&gt;Redis cache-first, SQL only on miss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache hit rate&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~97%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirect p95&lt;/td&gt;
&lt;td&gt;~180 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~5 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The concurrency story is one SQL statement — &lt;code&gt;UPDATE TOP (@blockSize) ... OUTPUT inserted.Code WITH (UPDLOCK, READPAST)&lt;/code&gt; — so many app servers check out disjoint blocks with no distributed lock, then serve each block from an in-memory queue. (Full &lt;code&gt;KeyGenerationService&lt;/code&gt; + cache-first minimal-API redirect code is in the post.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The model to carry forward
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A URL shortener is a key-generation problem stapled to a caching problem.&lt;/strong&gt; Three habits it teaches: (1) let the read:write ratio pick your architecture — 100:1 means "cache," full stop; (2) generate uniqueness ahead of time — pre-computing keys turns a collision problem into an O(1) hand-out; (3) name the 301 vs 302 trade-off.&lt;/p&gt;

&lt;p&gt;The full guide has all three approaches in depth, the base62 code, custom-alias handling, the capacity table, the design checklist, the full production .NET 9 implementation, and the "when this is overkill" honest section:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/design-a-url-shortener-system-design" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/design-a-url-shortener-system-design&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/design-a-url-shortener-system-design" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>interview</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Crack Any System Design Interview — A Repeatable 8-Step Framework</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Sun, 30 Aug 2026 16:42:18 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/how-to-crack-any-system-design-interview-a-repeatable-8-step-framework-3fh1</link>
      <guid>https://dev.to/kirandeepjassalcrypto/how-to-crack-any-system-design-interview-a-repeatable-8-step-framework-3fh1</guid>
      <description>&lt;p&gt;Most people don't fail a system design interview because they don't know the technology. They fail because they have &lt;strong&gt;no method&lt;/strong&gt;. They hear "Design Twitter," jump straight to "let's use a database," design one random corner in great detail, ignore the rest, and run out of time with a half-drawn box on the whiteboard. The candidates who pass aren't smarter — they follow a repeatable sequence, out loud, every single time.&lt;/p&gt;

&lt;p&gt;This is the condensed framework; the full guide (every step in depth, the building blocks, the numbers to memorize, and a full worked example) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/how-to-crack-system-design-interview-framework" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/how-to-crack-system-design-interview-framework&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why the question is open-ended on purpose
&lt;/h2&gt;

&lt;p&gt;"Design Twitter" has no single right answer, the scope is huge, and you have 45 minutes. That's the point — the interviewer isn't checking whether you memorized Twitter's architecture. They're watching &lt;strong&gt;how you navigate ambiguity, make trade-offs, and communicate.&lt;/strong&gt; Give them a clear, structured walk through the problem and you pass, even if the final design isn't "perfect."&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;Time (~45m)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Clarify requirements (functional + non-functional) and &lt;strong&gt;scope down&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Back-of-envelope &lt;strong&gt;estimates&lt;/strong&gt; (QPS, storage, bandwidth)&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;API design&lt;/strong&gt; — the handful of endpoints that matter&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Data model&lt;/strong&gt; — entities, SQL vs NoSQL and why&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;High-level architecture&lt;/strong&gt; — draw the boxes&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Deep-dive the hard part&lt;/strong&gt; — the 1–2 interesting things&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bottlenecks &amp;amp; scaling&lt;/strong&gt; — cache, shard, replicate, queue&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Trade-offs &amp;amp; wrap-up&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The steps that actually decide pass/fail
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Clarify + scope down.&lt;/strong&gt; Never start designing — start asking. Functional (what it does) + non-functional (scale, latency, consistency, read:write ratio). Then cut ruthlessly to 2–4 features and park the rest out loud. &lt;strong&gt;The single most common failure is skipping this and designing the wrong system beautifully.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Estimate.&lt;/strong&gt; You're after order of magnitude, not precision. The shortcut: there are ~86,400 s/day ≈ &lt;strong&gt;10^5&lt;/strong&gt;, so &lt;em&gt;requests/day ÷ 100,000 ≈ average QPS&lt;/em&gt;, and peak ≈ 2–3×.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,000,000,000 reads/day ÷ 86,400 ≈ 11,600 QPS average
Peak ≈ 3x ≈ ~35,000 QPS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one number already tells you: no single DB serves this — you need caching, replicas, sharding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3–4. API + data model.&lt;/strong&gt; Define the contract first (cursor-based pagination, not offset; explicit auth). Then pick a store and &lt;strong&gt;justify it&lt;/strong&gt; — "tweets are append-heavy, read by key, no joins, so a wide-column store scales better than relational here." The reasoning is graded, not the choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Draw the architecture.&lt;/strong&gt; Client → load balancer → &lt;strong&gt;stateless&lt;/strong&gt; app servers → cache/DB → response. Push heavy/slow work (media, notifications, fan-out) onto a &lt;strong&gt;queue + workers&lt;/strong&gt; so the request path stays fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deep-dive the ONE hard part.&lt;/strong&gt; Every "Design X" has 1–2 things that make it interesting; the rest is plumbing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;The hard part&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;URL shortener&lt;/td&gt;
&lt;td&gt;Short-code generation &amp;amp; collisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiter&lt;/td&gt;
&lt;td&gt;The algorithm + distributed counters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;News feed&lt;/td&gt;
&lt;td&gt;Fan-out on write vs read (celebrity problem)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chat&lt;/td&gt;
&lt;td&gt;Real-time delivery, ordering, presence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uber&lt;/td&gt;
&lt;td&gt;Geospatial indexing &amp;amp; matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Idempotency &amp;amp; the ledger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YouTube&lt;/td&gt;
&lt;td&gt;Transcoding pipeline &amp;amp; CDN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One well-reasoned deep-dive beats ten shallow boxes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7–8. Bottlenecks + trade-offs.&lt;/strong&gt; Ask "what breaks first?" → caching (name the invalidation), read replicas, sharding (state the shard key), async queues, CDN, remove single points of failure. Then close by naming the trade-offs you made and what you'd revisit. Raising bottlenecks before the interviewer does is a senior signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ~10 building blocks every problem reuses
&lt;/h2&gt;

&lt;p&gt;Load balancer · cache (Redis) · CDN · database (SQL for consistency, NoSQL for scale; replicas for read scale, sharding for write scale) · message queue (Kafka/SQS) · blob store (S3 — never put video in your DB) · consistent hashing · rate limiter / API gateway. Learn these cold and every "Design X" becomes an assembly problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers to memorize
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency:&lt;/strong&gt; RAM ~100 ns, SSD random read ~100 µs, same-DC round trip ~500 µs, HDD seek ~10 ms, &lt;strong&gt;cross-continent round trip ~150 ms&lt;/strong&gt;. Memory is fast, disk is slow, network is slower the farther it goes — so cache aggressively and keep data near users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Powers of two:&lt;/strong&gt; 2^10 ≈ 1 KB, 2^20 ≈ 1 MB, 2^30 ≈ 1 GB, 2^40 ≈ 1 TB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability:&lt;/strong&gt; 99.9% ≈ 8.7 h down/year · 99.99% ≈ 52 min · 99.999% ≈ 5 min. Each nine is ~10× harder — ask how many you actually need.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A system design interview is a guided conversation about trade-offs, not a test with an answer key.&lt;/strong&gt; The interviewer is your collaborator, not your examiner. Three habits separate a pass from a fail: clarify and scope before you design, let numbers drive decisions, and communicate relentlessly (no silent whiteboarding). Master the 8 steps, the 10 building blocks, and the handful of numbers, and "Design X" becomes the same well-worn walk applied to a new destination.&lt;/p&gt;

&lt;p&gt;The full guide has every step in depth, the 45-minute timeline, the full building-blocks diagram, the complete latency/powers-of-two/QPS tables, a full "Design Pastebin" worked example in one lap, and the where-the-framework-bends honest section:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/how-to-crack-system-design-interview-framework" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/how-to-crack-system-design-interview-framework&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/how-to-crack-system-design-interview-framework" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>interview</category>
      <category>programming</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>React Authentication &amp; Authorization — JWT, Refresh Token Rotation, RBAC, Azure AD, OAuth2 + PKCE (Real Code)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Sat, 29 Aug 2026 17:06:13 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/react-authentication-authorization-jwt-refresh-token-rotation-rbac-azure-ad-oauth2-pkce-1a5d</link>
      <guid>https://dev.to/kirandeepjassalcrypto/react-authentication-authorization-jwt-refresh-token-rotation-rbac-azure-ad-oauth2-pkce-1a5d</guid>
      <description>&lt;p&gt;Auth is the part of a React app where most teams get most of it right and one part dangerously wrong. The wrong part is usually the same: tokens in &lt;code&gt;localStorage&lt;/code&gt;, no refresh strategy, role checks scattered through the UI, and a backend that trusts whatever the client sends. It works in dev, passes review, and quietly enables account takeovers in production.&lt;/p&gt;

&lt;p&gt;This is the condensed, opinionated production playbook. The full guide (all the code, diagrams, the OAuth2+PKCE walkthrough, MSAL setup, and the architect's checklist) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/react-authentication-authorization-jwt-refresh-tokens-rbac-azure-ad-oauth2-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-authentication-authorization-jwt-refresh-tokens-rbac-azure-ad-oauth2-guide&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  AuthN vs AuthZ
&lt;/h2&gt;

&lt;p&gt;Two different problems that share a header:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; — &lt;em&gt;who are you?&lt;/em&gt; Verified once at login → a token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization&lt;/strong&gt; — &lt;em&gt;what can you do?&lt;/em&gt; Checked on &lt;em&gt;every&lt;/em&gt; request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  JWT — do this right or nothing else matters
&lt;/h2&gt;

&lt;p&gt;A JWT is &lt;code&gt;header.payload.signature&lt;/code&gt;. The signature is what makes it trustworthy — &lt;strong&gt;without verifying the signature on the server, a JWT is just JSON anyone can make up.&lt;/strong&gt; The rules that aren't optional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short lifetime&lt;/strong&gt; — 5–15 min. Refresh tokens handle long sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify on the server, always&lt;/strong&gt; — the client decodes for UI hints only; never trust client-side claims for authz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RS256 (asymmetric)&lt;/strong&gt; for SPAs — server signs with a private key, APIs verify with the public key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always check &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;&lt;/strong&gt; on the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to store the access token
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;XSS-safe?&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ any XSS reads it&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — one XSS = account takeover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sessionStorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ same risk&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-memory (module var / state)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — modern best practice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;HttpOnly&lt;/code&gt; cookie&lt;/td&gt;
&lt;td&gt;✅ JS can't read it&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (needs CSRF token)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The 2026 pattern:&lt;/strong&gt; access token in &lt;strong&gt;memory&lt;/strong&gt;, refresh token in an &lt;strong&gt;&lt;code&gt;HttpOnly&lt;/code&gt; cookie&lt;/strong&gt;. On reload, hit &lt;code&gt;/refresh&lt;/code&gt; to mint a new access token. Lost-on-reload is the cost; no-XSS-leak is the benefit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// auth/tokenStore.ts — in-memory access token&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&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;h2&gt;
  
  
  Refresh token rotation + reuse detection — the security cornerstone
&lt;/h2&gt;

&lt;p&gt;Every refresh issues a &lt;strong&gt;new&lt;/strong&gt; refresh token and invalidates the old one. If a revoked token is ever used again, it was stolen → revoke the entire chain immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t=0    RT1 issued at login          valid
t=10m  /refresh with RT1            mint RT2, RT1 revoked
t=20m  /refresh with RT2            mint RT3, RT2 revoked
t=21m  attacker uses stolen RT1     reuse detected -&amp;gt; revoke chain, force re-login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is OAuth 2.0 Refresh Token Rotation. Auth0, Okta, and Azure AD do it by default; if you roll your own, do this. Use a single in-flight refresh so a burst of 401s doesn't hammer &lt;code&gt;/refresh&lt;/code&gt; 50x in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pendingRefresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tryRefresh&lt;/span&gt;&lt;span class="p"&gt;()&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;pendingRefresh&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;pendingRefresh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// everyone shares one promise&lt;/span&gt;
  &lt;span class="nx"&gt;pendingRefresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/refresh&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&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="nf"&gt;then&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;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&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="nx"&gt;pendingRefresh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&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;pendingRefresh&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;h2&gt;
  
  
  RBAC — two layers, only one is real
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI LAYER (React)   -&amp;gt; hides buttons the user can't use -&amp;gt; purpose: UX      -&amp;gt; trust: NONE
SERVER LAYER (API) -&amp;gt; rejects requests the user can't make -&amp;gt; purpose: SECURITY -&amp;gt; trust: THE LINE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client-side role checks are UX, not security. A user can open DevTools, edit React state, and reveal hidden buttons. That's fine — when they click, the server returns 403.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PERMISSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post.write&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post.delete&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing.edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;billing_admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useCan&lt;/span&gt;&lt;span class="p"&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;user&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&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="nx"&gt;PERMISSIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server — same role map, but binding&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"post.delete"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/posts/{id}"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DeletePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The SPA's permission table and the server's policies must derive from the same source of truth.&lt;/strong&gt; When they drift, security holes appear silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth 2.0 + PKCE — the modern login
&lt;/h2&gt;

&lt;p&gt;PKCE (Proof Key for Code Exchange) is the SPA-safe extension that replaced the banned Implicit Flow. The SPA generates a &lt;code&gt;code_verifier&lt;/code&gt;, sends &lt;code&gt;SHA256(verifier)&lt;/code&gt; as the challenge, and proves ownership at token exchange. Without PKCE, an attacker who intercepts the code could redeem it. &lt;strong&gt;If a tutorial uses &lt;code&gt;response_type=token&lt;/code&gt;, it's pre-2019 — don't follow it.&lt;/strong&gt; For production, use the &lt;strong&gt;BFF (Backend For Frontend)&lt;/strong&gt; pattern: a thin server completes the token exchange and sets the refresh token in an HttpOnly cookie the SPA never sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure AD / Entra ID via MSAL
&lt;/h2&gt;

&lt;p&gt;For B2B / enterprise, Azure AD gives you SSO, MFA, conditional access, audit logs, and app-role claims that map straight to your RBAC — for free. MSAL wraps the OAuth2+PKCE flow and does silent refresh for you. One rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;cacheLocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;memoryStorage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// NOT localStorage, even though MSAL offers it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;App Roles show up as a &lt;code&gt;roles&lt;/code&gt; claim, so your &lt;code&gt;useCan&lt;/code&gt; hook works unchanged. &lt;strong&gt;One RBAC table, two enforcement points, one identity source.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Production metrics (90-engineer SaaS migration)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth security incidents / qtr&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth support tickets / qtr&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;31&lt;/strong&gt; (−78%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session before re-login&lt;/td&gt;
&lt;td&gt;8 h&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30 days&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to detect stolen token&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 1s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom auth code&lt;/td&gt;
&lt;td&gt;~2,400 lines&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~600&lt;/strong&gt; (−75%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to add a new SSO customer&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 hour&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;Auth is a system, not a feature. The SPA, API, and IdP must agree on &lt;strong&gt;one vocabulary&lt;/strong&gt; (roles, claims, tenant) and &lt;strong&gt;one trust boundary&lt;/strong&gt; (the server). Three habits keep it boring: treat the client as untrusted, use short access tokens + rotating refresh + HttpOnly cookies, and use the platform (Auth0/Azure AD/Cognito) unless you can name a reason that survives a security review.&lt;/p&gt;

&lt;p&gt;The full guide has the complete token store + AuthProvider, the apiFetch refresh-on-401 wrapper, route guards, the full OAuth2+PKCE code, MSAL React setup + app-role mapping, the 11-point security fix-list, and the architect's checklist:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/react-authentication-authorization-jwt-refresh-tokens-rbac-azure-ad-oauth2-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-authentication-authorization-jwt-refresh-tokens-rbac-azure-ad-oauth2-guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/react-authentication-authorization-jwt-refresh-tokens-rbac-azure-ad-oauth2-guide" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Event-Driven Architecture in .NET — Why, How, Real Code (ASP.NET Core + React) and When NOT to Use It</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Thu, 27 Aug 2026 17:20:55 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/event-driven-architecture-in-net-why-how-real-code-aspnet-core-react-and-when-not-to-use-2f8o</link>
      <guid>https://dev.to/kirandeepjassalcrypto/event-driven-architecture-in-net-why-how-real-code-aspnet-core-react-and-when-not-to-use-2f8o</guid>
      <description>&lt;p&gt;Most teams reach for event-driven architecture (EDA) because they heard it "scales." Then they discover the hard part: events arrive out of order, get delivered twice, and a bug three services away surfaces as a customer complaint with no stack trace. EDA is powerful — and it trades one set of problems for another.&lt;/p&gt;

&lt;p&gt;This is the honest, production version for .NET. This is the condensed take; the full guide (complete architecture diagram, real MassTransit code, the React SignalR live tracker, the tuning playbook, and the when-not-to section) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/event-driven-architecture-aspnet-core-react-production-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/event-driven-architecture-aspnet-core-react-production-guide&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What EDA actually is
&lt;/h2&gt;

&lt;p&gt;In request/response, &lt;code&gt;OrderService → (blocks) → PaymentService → (blocks) → InventoryService&lt;/code&gt;. If any link is slow or down, the whole chain stalls. In EDA, &lt;code&gt;OrderService&lt;/code&gt; emits a fact — &lt;code&gt;OrderPlaced&lt;/code&gt; — and moves on. Payment, inventory, email, and analytics all react independently through a broker. The producer doesn't know who's listening, doesn't wait, and doesn't break if a consumer is down. That decoupling is the whole point — and everything good &lt;em&gt;and&lt;/em&gt; hard about EDA flows from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it buys you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kills tight coupling.&lt;/strong&gt; Want a Slack alert on new orders? &lt;code&gt;SlackService&lt;/code&gt; subscribes to &lt;code&gt;OrderPlaced&lt;/code&gt;. &lt;code&gt;OrderService&lt;/code&gt; never changes. New consumers are &lt;em&gt;added&lt;/em&gt;, not &lt;em&gt;integrated&lt;/em&gt; — open/closed at the architecture level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kills blocking latency.&lt;/strong&gt; Checkout emits &lt;code&gt;OrderPlaced&lt;/code&gt; and returns a &lt;code&gt;202&lt;/code&gt; in tens of ms; the downstream work runs in parallel, async.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handles spiky load.&lt;/strong&gt; A flash sale queues in the broker (load leveling); consumers process at their own pace and you scale the bottleneck one independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Independent deployment + a free audit log&lt;/strong&gt; (the event stream &lt;em&gt;is&lt;/em&gt; the history).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs (be honest)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eventual consistency&lt;/strong&gt; — the order isn't confirmed &lt;em&gt;yet&lt;/em&gt;. If your domain needs strong consistency, EDA fights you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate delivery&lt;/strong&gt; — brokers guarantee &lt;em&gt;at-least-once&lt;/em&gt;, not exactly-once. The same event &lt;em&gt;will&lt;/em&gt; arrive twice. Every consumer with side effects must be idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-order delivery&lt;/strong&gt; — don't assume order; partition by entity key when ordering matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging across the boundary&lt;/strong&gt; — the failure is "the order never confirmed," cause three hops away. Distributed tracing is mandatory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational complexity&lt;/strong&gt; — a broker to run, queues to monitor, DLQs to drain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The two non-negotiables
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The Outbox.&lt;/strong&gt; The classic bug: save the order, then publish the event — but the process crashes &lt;em&gt;between&lt;/em&gt; them. Now you have an order with no event. Fix: write the event into the &lt;em&gt;same DB transaction&lt;/em&gt; as the business data, then a dispatcher publishes it after commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;db&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="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderPlaced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// through the Outbox&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// ONE commit: order + outbox event&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Accepted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/orders/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&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;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Pending"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Idempotent consumers.&lt;/strong&gt; At-least-once + retries guarantee duplicates. Charging twice is unacceptable — dedupe on message ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AlreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageId&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// skip duplicate&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChargeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MarkProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MessageId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The React payoff (SignalR, no polling)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;NotifyService&lt;/code&gt; consumes &lt;code&gt;OrderConfirmed&lt;/code&gt; and pushes it to the browser over SignalR; a &lt;code&gt;useOrderStatus&lt;/code&gt; hook flips the UI from "Pending" to "Confirmed" in real time as the async events flow through — no polling. The user saw a fast &lt;code&gt;202&lt;/code&gt;, then watches the order tick to Confirmed live.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production levers (each with a reason)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever&lt;/th&gt;
&lt;th&gt;Buys you&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Outbox&lt;/td&gt;
&lt;td&gt;No lost events&lt;/td&gt;
&lt;td&gt;One extra table + dispatcher&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency&lt;/td&gt;
&lt;td&gt;Safe aggressive retries&lt;/td&gt;
&lt;td&gt;A dedupe store (Redis)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefetch + concurrency&lt;/td&gt;
&lt;td&gt;Per-instance throughput&lt;/td&gt;
&lt;td&gt;Tune carefully or OOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partitioning (by OrderId)&lt;/td&gt;
&lt;td&gt;Ordering + parallelism&lt;/td&gt;
&lt;td&gt;Key design; can't reorder later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KEDA autoscaling&lt;/td&gt;
&lt;td&gt;Elastic capacity&lt;/td&gt;
&lt;td&gt;K8s + autoscaler setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DLQ + alerts&lt;/td&gt;
&lt;td&gt;Poison isolation + visibility&lt;/td&gt;
&lt;td&gt;Ops process to drain DLQ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backpressure&lt;/td&gt;
&lt;td&gt;Stability under spike&lt;/td&gt;
&lt;td&gt;Lower peak throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When NOT to use it
&lt;/h2&gt;

&lt;p&gt;Simple CRUD (a synchronous monolith is faster to build and easier to debug). Strong-consistency transactions (a bank balance correct the instant the call returns). Small teams without ops maturity. Low traffic with no scale problem. In all of these, EDA is complexity with no payoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decision rule:&lt;/strong&gt; adopt EDA when the cost of coupling and blocking exceeds the cost of eventual consistency and operational complexity. If you can't clearly say which side is heavier, you probably don't need it yet.&lt;/p&gt;

&lt;p&gt;The full guide has the complete architecture diagram, the full MassTransit + Azure Service Bus code (Outbox, idempotent consumer, saga completion), the React SignalR tracker end-to-end, all 8 tuning levers with reasoning, and the 10-step incremental adoption playbook:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/event-driven-architecture-aspnet-core-react-production-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/event-driven-architecture-aspnet-core-react-production-guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/event-driven-architecture-aspnet-core-react-production-guide" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>react</category>
    </item>
    <item>
      <title>Building a ChatGPT-Like App in React — Streaming, OpenAI, RAG, and pgvector (Real Code + Metrics)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Wed, 26 Aug 2026 18:40:31 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/building-a-chatgpt-like-app-in-react-streaming-openai-rag-and-pgvector-real-code-metrics-3a31</link>
      <guid>https://dev.to/kirandeepjassalcrypto/building-a-chatgpt-like-app-in-react-streaming-openai-rag-and-pgvector-real-code-metrics-3a31</guid>
      <description>&lt;p&gt;Building a ChatGPT-style assistant looks easy in a demo and breaks in three places when it goes to production: the &lt;em&gt;responses don't stream&lt;/em&gt; (users stare at a spinner), the model &lt;em&gt;hallucinates&lt;/em&gt; (it has no idea what's in your docs), and your &lt;em&gt;costs explode&lt;/em&gt;. The difference between a demo and a shippable assistant is four techniques composed correctly.&lt;/p&gt;

&lt;p&gt;I built a real SaaS "ask the docs" assistant in React and measured it. This is the condensed version; the full guide (the streaming route, the RAG pipeline, the vector-store decisions, and every metric) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/react-ai-chatgpt-like-application-streaming-openai-rag-vector-databases-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-ai-chatgpt-like-application-streaming-openai-rag-vector-databases-guide&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The result (real SaaS docs assistant, ~3,000 docs, ~50k questions/mo)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (just LLM)&lt;/th&gt;
&lt;th&gt;After (streaming + RAG + pgvector)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination rate&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Irrelevant answers&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer cites a source&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"This was helpful"&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;84%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first token&lt;/td&gt;
&lt;td&gt;3,500ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;250ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgvector retrieval (p95)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;65ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per query&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.003&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Support tickets deflected&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23% of product Qs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The four techniques
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Streaming — non-negotiable UX, ship it first.&lt;/strong&gt; A full response is a 3–13s wait; the first token lands in ~250ms. That took time-to-first-content 3,500ms → 250ms and abandon rate 14% → 3%. Use &lt;strong&gt;Server-Sent Events&lt;/strong&gt;, not WebSocket — one-way server→client is all chat needs, and it's what OpenAI's own API uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. OpenAI behind a server route — never the browser.&lt;/strong&gt; The API key stays server-side, and the route is where rate limiting (per tenant + per user), prompt validation, cost caps, and observability live. Edge runtime streams best; log usage &lt;em&gt;after&lt;/em&gt; the stream closes, never blocking on telemetry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. RAG stops hallucinations.&lt;/strong&gt; Retrieve the relevant chunks from &lt;em&gt;your&lt;/em&gt; docs, put them in the prompt, instruct the model to answer only from them. Hallucinations 18% → 4%, "helpful" 51% → 84%, and it deflected 23% of incoming support questions. The underrated lever is chunking + retrieval quality, not the model — a tuned RAG pipeline on gpt-4o-mini beats a naive one on gpt-4o, for less money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. pgvector is enough for almost everyone in 2026.&lt;/strong&gt; The Postgres you already run, with an HNSW index → sub-100ms retrieval (p95 65ms) up to ~10M vectors. Resist adding a managed vector DB before you've tried Postgres; reach for Pinecone/Qdrant only at higher scale or specific latency needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React (useChat) --POST /api/chat (SSE stream)--&amp;gt; Next.js API route
   |                                               1. rate-limit + validate
   |                                               2. embed question (OpenAI)
   |                                               3. retrieve top-k (pgvector cosine)
   |                                               4. build grounded prompt
   &amp;lt;---- tokens stream back -----------------------  5. stream from gpt-4o-mini
                                                    6. log usage + cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first SSE event sends the retrieved &lt;strong&gt;sources&lt;/strong&gt; so the UI shows citation chips within ~100ms while the answer streams in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The economics
&lt;/h2&gt;

&lt;p&gt;Cost per query landed at &lt;strong&gt;$0.003&lt;/strong&gt;. A response cache on the question hash (34% of questions are repeats) saves &lt;strong&gt;~$4,200/month at 1M queries&lt;/strong&gt; — without it the bill is 50% higher. One warning: watch the cost dashboard daily the first month. A single tenant looping the API can run a five-figure bill before you notice. Set hard per-tenant caps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;A production AI assistant is &lt;strong&gt;four boring components composed correctly&lt;/strong&gt;: a streaming UI, a server-side LLM call, retrieval over your own data, and a vector store to make that retrieval fast. None is exotic in 2026 — the engineering is making them work &lt;em&gt;together&lt;/em&gt;: fast enough to feel instant, accurate enough to be trusted, cheap enough to scale. Ship streaming + RAG on day one; everything else is optimization.&lt;/p&gt;

&lt;p&gt;The full guide has the complete streaming Next.js route, the manual + Vercel-AI-SDK React &lt;code&gt;useChat&lt;/code&gt;, the RAG pipeline (chunking, embeddings, pgvector storage &amp;amp; retrieval), the HNSW vs IVF decision, prompt-injection defenses, cost monitoring, and the full latency/cost/quality/reliability tables:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/react-ai-chatgpt-like-application-streaming-openai-rag-vector-databases-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-ai-chatgpt-like-application-streaming-openai-rag-vector-databases-guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/react-ai-chatgpt-like-application-streaming-openai-rag-vector-databases-guide" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Building Enterprise React Apps with Feature-Based Architecture (Real Code + Production Metrics)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Mon, 24 Aug 2026 16:27:26 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/building-enterprise-react-apps-with-feature-based-architecture-real-code-production-metrics-hjm</link>
      <guid>https://dev.to/kirandeepjassalcrypto/building-enterprise-react-apps-with-feature-based-architecture-real-code-production-metrics-hjm</guid>
      <description>&lt;p&gt;Every React app that survives long enough hits the same wall: the &lt;code&gt;components/&lt;/code&gt; folder has 240 files, every change touches a dozen of them, three teams step on each other in &lt;code&gt;utils/&lt;/code&gt;, new hires take three weeks to find the right hook. The codebase didn't get worse — it got &lt;em&gt;bigger&lt;/em&gt;, and the architecture stopped scaling.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;feature-based architecture&lt;/strong&gt;: organize by &lt;em&gt;capability&lt;/em&gt; (billing, users, reports) instead of by &lt;em&gt;kind&lt;/em&gt; (components, hooks, services). I migrated a real enterprise SaaS app this way and measured it. This is the condensed version; the full guide (the 4 rules, a complete feature in code, the lazy-load splits, and the migration playbook) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/enterprise-react-feature-based-architecture-real-code-metrics" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/enterprise-react-feature-based-architecture-real-code-metrics&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The result (same app, 240 components, before + after)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (type-based)&lt;/th&gt;
&lt;th&gt;After (feature-based)&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build time (Vite, cold)&lt;/td&gt;
&lt;td&gt;145s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−37%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial JS (gzipped)&lt;/td&gt;
&lt;td&gt;1.2 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;380 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routes loading &amp;gt; 1 MB&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;gone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files touched per PR&lt;/td&gt;
&lt;td&gt;12.4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−59%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge-conflict rate&lt;/td&gt;
&lt;td&gt;21%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onboarding to first PR&lt;/td&gt;
&lt;td&gt;18 days&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7 days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−61%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-feature bugs/qtr&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−83%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why type-based folders break
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;components/&lt;/code&gt; &lt;code&gt;hooks/&lt;/code&gt; &lt;code&gt;services/&lt;/code&gt; works to ~30 components. Past that: ownership is fuzzy ("who owns &lt;code&gt;useUserPermissions&lt;/code&gt;?"), every change touches everything, teams collide in &lt;code&gt;utils/&lt;/code&gt;, dead code accumulates, and bundle splitting is impossible because billing is scattered across 8 folders. The question that matters — "where does the &lt;em&gt;billing&lt;/em&gt; feature live?" — has the answer "everywhere."&lt;/p&gt;

&lt;h2&gt;
  
  
  The structure: organize by capability
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── shared/        (ui, lib, api — cross-cutting, NO business logic)
├── features/
│   ├── auth/      (api, components, hooks, store, types, index.ts)
│   ├── billing/   (api, components, hooks, store, types, index.ts)
│   ├── users/
│   └── reports/
└── app/           (router, providers — composition root)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each feature is self-contained with a single public entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 rules that make it work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. One public &lt;code&gt;index.ts&lt;/code&gt; per feature.&lt;/strong&gt; It exposes 4 exports; the feature has 20+ internal files. That ratio &lt;em&gt;is&lt;/em&gt; the boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No cross-feature imports except via that index.&lt;/strong&gt; Enforce with ESLint &lt;code&gt;no-restricted-imports&lt;/code&gt; on &lt;code&gt;features/*/*&lt;/code&gt; — at PR time, not in code review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cross-cutting code lives in &lt;code&gt;shared/&lt;/code&gt;, with no business logic.&lt;/strong&gt; &lt;code&gt;Button&lt;/code&gt;/&lt;code&gt;Modal&lt;/code&gt; → &lt;code&gt;shared/ui&lt;/code&gt;. &lt;code&gt;formatInvoice()&lt;/code&gt; → &lt;code&gt;features/billing/lib&lt;/code&gt;. The test: if it names a domain concept, it's a feature concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Features depend on &lt;code&gt;shared/&lt;/code&gt; only, never each other.&lt;/strong&gt; Billing needs the user? It takes a &lt;code&gt;userId&lt;/code&gt;, not an import from &lt;code&gt;auth&lt;/code&gt;. Autonomy is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The biggest win: natural code-splitting
&lt;/h2&gt;

&lt;p&gt;Because each feature is self-contained, lazy-loading is one line at the composition root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BillingRoutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/features/billing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BillingRoutes&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;AdminRoutes&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/features/admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AdminRoutes&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user who never opens &lt;code&gt;/admin/*&lt;/code&gt; never downloads the admin code. That's what took the initial bundle &lt;strong&gt;1.2 MB → 380 KB&lt;/strong&gt; — impossible when billing is scattered across 8 folders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrate incrementally, never a rewrite
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;shared/&lt;/code&gt;/&lt;code&gt;features/&lt;/code&gt;/&lt;code&gt;app/&lt;/code&gt; alongside the old folders (they coexist). Move the smallest, most isolated feature first (usually &lt;code&gt;auth&lt;/code&gt;). Lock its boundary with ESLint. Repeat per feature, largest pain first. Drain the old folders. Each step ships independently — no big-bang PR. ~3 sprints for one squad in spare cycles, payback in ~6 weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;Below ~20 components this is bureaucracy — don't impose it. Above ~50 with multiple teams, it's survival. And discipline is the architecture: without the ESLint boundary rule, "feature folders" decays into "type folders with a different name" in six months.&lt;/p&gt;

&lt;p&gt;The mental model: type-based folders optimize for "where do I put a hook?" Feature-based folders optimize for "where does this &lt;em&gt;capability&lt;/em&gt; live?" Get that right and your app stops getting harder to change as it gets bigger — the actual definition of architecture working.&lt;/p&gt;

&lt;p&gt;The full guide has the 4 rules in depth, a complete &lt;code&gt;billing&lt;/code&gt; feature end-to-end (types, api, hooks, components, routes, tests), the bundle-split output, the state-per-feature pattern, the migration playbook, and the full metrics matrix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/enterprise-react-feature-based-architecture-real-code-metrics" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/enterprise-react-feature-based-architecture-real-code-metrics&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/enterprise-react-feature-based-architecture-real-code-metrics" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>React Design Patterns Every Architect Should Know in 2026 (Real Code + Production Metrics)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Sat, 22 Aug 2026 07:18:49 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/react-design-patterns-every-architect-should-know-in-2026-real-code-production-metrics-fj1</link>
      <guid>https://dev.to/kirandeepjassalcrypto/react-design-patterns-every-architect-should-know-in-2026-real-code-production-metrics-fj1</guid>
      <description>&lt;p&gt;A senior React developer can write any component. An &lt;strong&gt;architect&lt;/strong&gt; knows &lt;em&gt;which pattern&lt;/em&gt; to reach for, &lt;em&gt;when&lt;/em&gt;, and (crucially) &lt;em&gt;when not to&lt;/em&gt;. Picking the wrong abstraction is one of the most expensive mistakes in a frontend codebase — it locks in shapes that end up everywhere, and untangling them costs months.&lt;/p&gt;

&lt;p&gt;So I took a real production SaaS dashboard and consolidated it on the right patterns, measuring the impact. This is the condensed version; the full guide (all six patterns with before/after code, the composition diagram, the decision flow, and anti-patterns) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/react-design-patterns-container-hooks-compound-render-props-hoc-provider-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-design-patterns-container-hooks-compound-render-props-hoc-provider-guide&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The result (same app, before + after)
&lt;/h2&gt;

&lt;p&gt;Moving from "HOC + render-props everywhere" → "Hooks + Compound + Provider":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Component code lines&lt;/td&gt;
&lt;td&gt;28,400&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20,500&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−28%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg component reuse&lt;/td&gt;
&lt;td&gt;2.1×&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.8×&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+176%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HOCs in codebase&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render-prop components&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−82%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom hooks&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;47&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prop-drilling bugs/qtr&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−63%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storybook without mocks&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;84%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+121%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New-dev first feature&lt;/td&gt;
&lt;td&gt;9 days&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5 days&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevTools tree depth&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;−50%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The 6 patterns, in one line each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Custom Hooks — the most important pattern.&lt;/strong&gt; They replaced most of what HOCs and render props did. The rule: if you can name the behavior as a noun ("the searchable list", "the form draft"), it's a hook. Master these first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Compound Components — HTML-shaped, composable APIs.&lt;/strong&gt; &lt;code&gt;&amp;lt;Tabs&amp;gt;&amp;lt;Tab/&amp;gt;&amp;lt;/Tabs&amp;gt;&lt;/code&gt; sharing state via context. The whole Radix/shadcn design language is built on this. Best for design-system components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Provider Pattern — app-scope concerns.&lt;/strong&gt; Theme, auth, locale, feature flags. Still essential — but not a poor man's state manager (Context re-renders every consumer; use Zustand for high-frequency state).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Container/Presentation — now Server + Client Components.&lt;/strong&gt; The principle (data vs view) survived; the syntax modernized. Server Component = container, &lt;code&gt;'use client'&lt;/code&gt; = presentation. Splitting them took Storybook-without-mocks 38% → 84%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Render Props — mostly obsolete.&lt;/strong&gt; Hooks beat it on nesting, composition, and TS. Survives only for true inversion-of-control (virtualizers, &lt;code&gt;&amp;lt;Form&amp;gt;{form =&amp;gt; ...}&amp;lt;/Form&amp;gt;&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. HOC — the demoted one.&lt;/strong&gt; Wrapper-hell, prop collisions, messy types. If a new HOC appears in a 2026 PR, ask "could this be a hook?" — almost always yes. Survives only for legacy class components and library boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the wins came from
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Custom hooks replacing HOC + RP duplication      ~38%
Server/Client (modern container/presentation)     ~22%
Compound components in the design system          ~16%
Providers replacing prop-drilling                  ~12%
Test-without-mocks (presentational components)     ~8%
Smaller DevTools tree (HOC removal)                ~4%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;Patterns aren't a checklist to use — they're a &lt;strong&gt;vocabulary for shapes of problems&lt;/strong&gt;. Know which shape you're looking at, reach for the matching pattern deliberately (before someone reinvents it badly in a PR), and the team stops re-solving the same thing six ways. A team with a coherent pattern vocabulary moves 2× faster than one without.&lt;/p&gt;

&lt;p&gt;Three habits that make it work: (1) write the vocabulary down (a &lt;code&gt;patterns.md&lt;/code&gt;), (2) prefer hooks until forced otherwise, (3) compose patterns — a real app uses all six where each fits, and can be fully coherent without ever writing a new HOC.&lt;/p&gt;

&lt;p&gt;The full guide has all six patterns with before/after code, the composition diagram, the full metrics table, the decision flow, and the anti-patterns table:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/react-design-patterns-container-hooks-compound-render-props-hoc-provider-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-design-patterns-container-hooks-compound-render-props-hoc-provider-guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/react-design-patterns-container-hooks-compound-render-props-hoc-provider-guide" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>React Performance Optimization — Every Technique with Before/After Code + Real Production Metrics</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:01:18 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/react-performance-optimization-every-technique-with-beforeafter-code-real-production-metrics-5ap7</link>
      <guid>https://dev.to/kirandeepjassalcrypto/react-performance-optimization-every-technique-with-beforeafter-code-real-production-metrics-5ap7</guid>
      <description>&lt;p&gt;"Make it faster" is the vaguest ticket in frontend. Faster &lt;em&gt;what&lt;/em&gt;? Measured &lt;em&gt;how&lt;/em&gt;? Most React perf advice is a pile of &lt;code&gt;useMemo&lt;/code&gt; everywhere and a vague feeling of virtue — with no numbers to prove it helped (and sometimes it made things worse).&lt;/p&gt;

&lt;p&gt;So I took a &lt;strong&gt;real production-shaped app&lt;/strong&gt; — a SaaS analytics dashboard with a 12,000-row table, live filters, and charts — profiled its actual bottlenecks, and fixed them one technique at a time, measuring before/after each. This is the condensed version; the full guide (every technique with before/after code, diagrams, and the full metrics matrix) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/react-performance-optimization-techniques-before-after-metrics-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-performance-optimization-techniques-before-after-metrics-guide&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The result (same app, profiled before + after)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Baseline&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LCP (load)&lt;/td&gt;
&lt;td&gt;4.1s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.3s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;68% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INP (interactivity)&lt;/td&gt;
&lt;td&gt;420ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;79% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS bundle (gzipped)&lt;/td&gt;
&lt;td&gt;880 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;240 KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;73% smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter re-render&lt;/td&gt;
&lt;td&gt;678ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;97% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table mount&lt;/td&gt;
&lt;td&gt;1,400ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;45ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;97% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scroll FPS&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;smooth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory (heap)&lt;/td&gt;
&lt;td&gt;310 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;120 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;61% less&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where the wins actually came from
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Virtualization             ~35%
Code splitting + bundle     ~25%
Server Components / SSR      ~18%
Re-render elimination        ~12%
Concurrent features           ~6%
Smaller wins                  ~4%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The top &lt;strong&gt;three structural techniques delivered ~78% of the total gains.&lt;/strong&gt; Scattering &lt;code&gt;useMemo&lt;/code&gt; everywhere — what most perf posts lead with — was the smallest bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  The techniques, briefly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Virtualization — the biggest single win.&lt;/strong&gt; The user sees ~30 rows; rendering the other 11,970 is waste. &lt;code&gt;@tanstack/react-virtual&lt;/code&gt; took DOM nodes 96,000 → ~320. Virtualize any list over ~200 rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Code-splitting + bundle trimming.&lt;/strong&gt; &lt;code&gt;React.lazy&lt;/code&gt; + &lt;code&gt;Suspense&lt;/code&gt; for the heavy export/settings modals (most users never open them), plus &lt;code&gt;moment&lt;/code&gt;→&lt;code&gt;date-fns&lt;/code&gt; and &lt;code&gt;lodash&lt;/code&gt;→&lt;code&gt;lodash/x&lt;/code&gt;. Bundle 880KB → 240KB, parse 2.3s → 0.7s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Server Components / SSR / streaming.&lt;/strong&gt; Fetch on the server, stream the shell instantly. Time-to-first-content 1.1s → 0.2s, client JS ~60% less, fetch waterfall eliminated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Re-render elimination.&lt;/strong&gt; &lt;code&gt;memo&lt;/code&gt; + stable &lt;code&gt;useCallback&lt;/code&gt;/&lt;code&gt;useMemo&lt;/code&gt; stopped the chart panel re-rendering on every keystroke. But memo isn't free — apply where the Profiler shows a real repeated expensive re-render, not everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Concurrent features.&lt;/strong&gt; &lt;code&gt;useTransition&lt;/code&gt; / &lt;code&gt;useDeferredValue&lt;/code&gt; keep the input snappy while filtering — INP 180ms → 90ms, zero dropped frames.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. React 19 Compiler.&lt;/strong&gt; Auto-memoizes for you, so most manual &lt;code&gt;useMemo&lt;/code&gt;/&lt;code&gt;useCallback&lt;/code&gt; becomes noise you can delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;React performance isn't "add &lt;code&gt;useMemo&lt;/code&gt; and hope." It's a loop: &lt;strong&gt;profile → find the real bottleneck → apply the right technique → re-measure → keep it only if the number moved.&lt;/strong&gt; Reach for structural wins (virtualize, split, server-render) first — they deliver 10× what scattered memoization does.&lt;/p&gt;

&lt;p&gt;The full guide has every technique with before/after code, the diagrams, the complete metrics matrix, and the decision flow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/react-performance-optimization-techniques-before-after-metrics-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-performance-optimization-techniques-before-after-metrics-guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/react-performance-optimization-techniques-before-after-metrics-guide" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
    </item>
    <item>
      <title>React State Management in 2026 — Context API vs Redux Toolkit vs Zustand vs Jotai (Same Cart, Real Code + Benchmarks)</title>
      <dc:creator>kirandeepjassal-crypto</dc:creator>
      <pubDate>Wed, 19 Aug 2026 18:20:45 +0000</pubDate>
      <link>https://dev.to/kirandeepjassalcrypto/react-state-management-in-2026-context-api-vs-redux-toolkit-vs-zustand-vs-jotai-same-cart-real-1d2a</link>
      <guid>https://dev.to/kirandeepjassalcrypto/react-state-management-in-2026-context-api-vs-redux-toolkit-vs-zustand-vs-jotai-same-cart-real-1d2a</guid>
      <description>&lt;p&gt;The React state-management debate has produced more bad takes than any other frontend topic. "Just use Context." "Redux is dead." "Zustand for everything." "Jotai is the future." All four are &lt;em&gt;partially&lt;/em&gt; right and &lt;em&gt;partially&lt;/em&gt; dangerous, depending on what you're building.&lt;/p&gt;

&lt;p&gt;So instead of arguing, I built the &lt;strong&gt;same shopping cart&lt;/strong&gt; — derived totals, async fetch, &lt;code&gt;localStorage&lt;/code&gt; persistence, three subscribing components — in &lt;strong&gt;all four libraries&lt;/strong&gt;, and benchmarked it. This is the condensed version; the full guide (all four implementations with real code, the complete matrix, and the decision flow) is on my site 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Full guide:&lt;/strong&gt; &lt;a href="https://prepstack.co.in/blog/react-state-management-context-redux-toolkit-zustand-jotai-comparison-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-state-management-context-redux-toolkit-zustand-jotai-comparison-guide&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The one benchmark that reframes everything
&lt;/h2&gt;

&lt;p&gt;1,000 components subscribed to one store. Update one value. How many re-render?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Components re-rendered&lt;/th&gt;
&lt;th&gt;Wall-clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context (single value)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,000&lt;/strong&gt; (all)&lt;/td&gt;
&lt;td&gt;42 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context (split into 5)&lt;/td&gt;
&lt;td&gt;~200&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redux Toolkit (selectors)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zustand (selector)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jotai (atom)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Context without splitting re-renders the world. The other three are within margin of each other — meaning the real differences are &lt;strong&gt;boilerplate and DX&lt;/strong&gt;, not render speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four, in one line each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Context API&lt;/strong&gt; — built-in, 0 KB, but every consumer re-renders on any change. Right for theme/auth/locale; wrong for anything busy or with many subscribers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redux Toolkit&lt;/strong&gt; — ~22 KB, most boilerplate, but RTK Query (caching, dedupe, invalidation), middleware, and time-travel DevTools are best-in-class. Payoff scales with app complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zustand&lt;/strong&gt; — ~3 KB, no provider, selectors built in, a full store (state + async + persistence) in ~25 lines. The modern default for most 2026 apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jotai&lt;/strong&gt; — state is many small atoms, each with its own subscriber list. Smallest blast radius per update; ideal for forms and derived graphs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real production migration (same e-commerce app)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Context-everywhere&lt;/th&gt;
&lt;th&gt;Redux Toolkit&lt;/th&gt;
&lt;th&gt;Zustand&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial JS (gzipped)&lt;/td&gt;
&lt;td&gt;412 KB&lt;/td&gt;
&lt;td&gt;438 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;390 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add-to-cart INP&lt;/td&gt;
&lt;td&gt;180 ms&lt;/td&gt;
&lt;td&gt;95 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter-by-category render&lt;/td&gt;
&lt;td&gt;290 ms&lt;/td&gt;
&lt;td&gt;60 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;52 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of store code&lt;/td&gt;
&lt;td&gt;~3,200&lt;/td&gt;
&lt;td&gt;~4,100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1,650&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New-dev "first feature shipped"&lt;/td&gt;
&lt;td&gt;6 days&lt;/td&gt;
&lt;td&gt;11 days&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4 days&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A separate forms project moving Redux → Jotai dropped re-render counts &lt;strong&gt;~75%&lt;/strong&gt; thanks to per-atom granularity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plot twist most debates miss
&lt;/h2&gt;

&lt;p&gt;In 2026, &lt;strong&gt;most of what people call "global state" is actually server state&lt;/strong&gt; — API data that belongs in &lt;strong&gt;TanStack Query&lt;/strong&gt; (or RTK Query), not any of these four. Query libs handle caching, dedupe, background refresh, retries, optimistic updates. Put server data there and an entire class of cache-invalidation bugs disappears. The four libraries above only fight over the remaining ~20% of true client state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Separate server state from client state first. Then: &lt;strong&gt;Context&lt;/strong&gt; for provider-shaped global config, &lt;strong&gt;Zustand&lt;/strong&gt; for most client state, &lt;strong&gt;Jotai&lt;/strong&gt; when it's atomic, &lt;strong&gt;Redux Toolkit&lt;/strong&gt; when you need its ecosystem. Do that and "state architecture" stops being a 3-week debate and becomes a 30-second decision per piece of state.&lt;/p&gt;

&lt;p&gt;The full guide has all four cart implementations with real code, the complete comparison matrix, the benchmark methodology, and the decision flow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://prepstack.co.in/blog/react-state-management-context-redux-toolkit-zustand-jotai-comparison-guide" rel="noopener noreferrer"&gt;https://prepstack.co.in/blog/react-state-management-context-redux-toolkit-zustand-jotai-comparison-guide&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://prepstack.co.in/blog/react-state-management-context-redux-toolkit-zustand-jotai-comparison-guide" rel="noopener noreferrer"&gt;PrepStack&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
