<?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: Kishan Agarwal </title>
    <description>The latest articles on DEV Community by Kishan Agarwal  (@kishanag028).</description>
    <link>https://dev.to/kishanag028</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%2F3959579%2F047e8953-6397-43f8-b972-31d79bb1a6f8.jpg</url>
      <title>DEV Community: Kishan Agarwal </title>
      <link>https://dev.to/kishanag028</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kishanag028"/>
    <language>en</language>
    <item>
      <title>How WhatsApp Works Without Internet: Offline Messaging and Sync Explained</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Tue, 16 Jun 2026 07:01:00 +0000</pubDate>
      <link>https://dev.to/kishanag028/how-whatsapp-works-without-internet-offline-messaging-and-sync-explained-4c3n</link>
      <guid>https://dev.to/kishanag028/how-whatsapp-works-without-internet-offline-messaging-and-sync-explained-4c3n</guid>
      <description>&lt;p&gt;Before we get overwhelmed by phrases like "eventual consistency" and "offline-first architecture," let's just talk about something you've probably done a hundred times. You're on a train through a tunnel, the signal drops to zero, and you send a message anyway. WhatsApp shows a single grey tick almost immediately. The message didn't go anywhere — there's no internet. So what just happened?&lt;/p&gt;

&lt;p&gt;That single tick is doing a lot of quiet engineering work. And understanding it unlocks how a huge category of modern apps think about reliability, state, and user trust.&lt;/p&gt;

&lt;p&gt;This is not as mysterious as it sounds. Let's walk through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moment You Hit Send With No Internet
&lt;/h2&gt;

&lt;p&gt;Picture this: you're on a flight, airplane mode is on, and you type "landed safely, will call soon" and tap send. WhatsApp shows the message instantly in your chat — a single grey tick. The app didn't freeze. It didn't say "no network, try again." It just... worked.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The obvious question here is: where did that message actually go?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It went nowhere yet. It went into your phone's local storage — a small database sitting right on your device. WhatsApp writes your message to this local store first, marks it as "pending," and shows it to you immediately. The network hasn't been involved at all.&lt;/p&gt;

&lt;p&gt;This is the core idea behind &lt;strong&gt;offline-first design&lt;/strong&gt;: the app treats your local device as the source of truth, not the server. You write locally, you sync later.&lt;/p&gt;

&lt;p&gt;Sounds simple. The engineering to make it reliable is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Phone Is a Mini Post Office
&lt;/h2&gt;

&lt;p&gt;Think of your phone as a local post office in a small town cut off by floods. You walk in, drop a letter, and get a receipt (the grey tick). The letter sits in the post office's outbox. The moment the roads reopen, the truck leaves and delivers everything.&lt;/p&gt;

&lt;p&gt;The post office doesn't tell you, "Sorry, roads are closed, come back later." It accepts your letter, stores it, and handles delivery when it can. That's exactly what the message queue on your device does.&lt;/p&gt;

&lt;p&gt;Every message you send while offline enters this queue — an ordered list of pending actions your app will retry as soon as connectivity returns. The queue persists even if you close the app, because it's written to local storage (usually SQLite on mobile), not just held in memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnq707vyw82ukz9n46sff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnq707vyw82ukz9n46sff.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Local Storage" Actually Means Here
&lt;/h2&gt;

&lt;p&gt;WhatsApp (and apps like it) keep a local copy of your entire recent conversation history on your device. This isn't just a cache for speed — it's a deliberate design decision, so the app can work without a server.&lt;/p&gt;

&lt;p&gt;When you open a chat, you're reading from this local database. When you type and send, you're writing to it. The server sync happens in the background, asynchronously.&lt;/p&gt;

&lt;p&gt;The local database tracks a few critical things for each message:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The message content&lt;/li&gt;
&lt;li&gt;A unique ID generated on your device&lt;/li&gt;
&lt;li&gt;A timestamp from the moment you wrote it&lt;/li&gt;
&lt;li&gt;A delivery status: &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That device-generated ID is more important than it looks. It's what lets the server recognize your message as unique even if it receives it multiple times (say, after a network retry). No duplicates, no confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Queue Comes Alive When You Reconnect
&lt;/h2&gt;

&lt;p&gt;The moment your phone gets a signal back — WiFi, 4G, doesn't matter — the app's sync process kicks in. It reads through the pending queue, oldest first, and starts pushing messages to the server.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Now wait a minute — what if two messages were queued up and they arrive at the server out of order?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Good catch. This is why the device-generated timestamp matters. The server uses it to reconstruct the correct message order, even if network delivery got scrambled. The queue on your device sends in order; the timestamp is a fallback guarantee.&lt;/p&gt;

&lt;p&gt;Once the server receives a message and stores it, it sends back an acknowledgement. Your app updates that message's status to &lt;code&gt;sent&lt;/code&gt; — and the grey tick becomes a double grey tick.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgi2b1a788f5zg0px5ew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgi2b1a788f5zg0px5ew.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Ticks: What Each State Actually Means
&lt;/h2&gt;

&lt;p&gt;This is where offline messaging connects to something you see every single day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single grey tick — Sent.&lt;/strong&gt; Your device has written the message to the server. That's it. The recipient hasn't been involved yet. If they're offline, it just means the server is holding it for them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double grey tick — Delivered.&lt;/strong&gt; The server pushed the message to the recipient's device, and their WhatsApp app acknowledged receipt. The message now lives in their local database too. The recipient may not have opened it — delivered just means their phone got it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Double blue tick — Read:&lt;/strong&gt; The recipient opened the conversation. Their app sent a read receipt back to the server, which relayed it to you. Blue ticks require the recipient to actively open the chat.&lt;/p&gt;

&lt;p&gt;Each state is a separate network event. Each can be delayed independently. That's why you sometimes see a message go from single tick to double blue all at once — the recipient was offline when it was delivered, then opened it later, and both events reached you simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Media Is a Separate Problem
&lt;/h2&gt;

&lt;p&gt;A text message is a few hundred bytes. A video can be 50MB. Offline handling for media is fundamentally different.&lt;/p&gt;

&lt;p&gt;When you send a photo while offline, the app queues an &lt;em&gt;upload intent&lt;/em&gt; — not the photo itself. The local database notes: "When we're back online, upload this file from this local path, then send the message that references it."&lt;/p&gt;

&lt;p&gt;When connectivity returns, the media uploads first to WhatsApp's servers. Once the upload completes and the server gives back a URL, the text message referencing that URL is sent. Only then does the recipient see the media.&lt;/p&gt;

&lt;p&gt;Sounds like a massive headache, right? But this separation is intentional. If both the upload and the send were one atomic operation, a failed upload midway would mean starting over completely. By splitting them, the app can resume an interrupted upload from where it left off — not from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Engineering Part
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Conflict Resolution and Message Ordering
&lt;/h3&gt;

&lt;p&gt;Here's where it gets genuinely tricky. Imagine you and a friend both go offline at the same time and both send messages to a group chat. When you both reconnect, whose messages get sent first? What order do they appear in?&lt;/p&gt;

&lt;p&gt;The short answer is: the server decides, and it uses timestamps to do it. But timestamps from different devices are never perfectly synchronized. Two phones with a 2-second clock drift can disagree on which message came first.&lt;/p&gt;

&lt;p&gt;Most messaging systems solve this with a technique called &lt;strong&gt;logical ordering&lt;/strong&gt; — the server assigns a monotonically increasing sequence number to each message as it arrives. This becomes the "official" order, regardless of what the device clocks said. Your local UI then reorders messages to match the server's sequence.&lt;/p&gt;

&lt;p&gt;This creates an interesting moment you may have noticed: you send a message, it shows at the bottom of your chat, then jumps slightly upward after syncing. That's message reordering happening in real time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eventual Consistency at the User Level
&lt;/h3&gt;

&lt;p&gt;Eventual consistency is a scary-sounding term from distributed systems. In plain English, it means: not everyone will see the same state at the same moment, but eventually, everyone will agree.&lt;/p&gt;

&lt;p&gt;WhatsApp is eventually consistent. If you send a message to a group, some members might get it in 2 seconds, others in 30 seconds. For a moment, the group chat looks different on different phones. But within some window of time — usually seconds — everyone catches up.&lt;/p&gt;

&lt;p&gt;The tradeoff being made here is &lt;strong&gt;availability over perfect real-time accuracy&lt;/strong&gt;. The app prioritizes letting you send and receive messages even under bad conditions, accepting that momentary inconsistency is less annoying than failure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fakin42kxke67td2w5agn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fakin42kxke67td2w5agn.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Catch: When Offline-First Gets Complicated
&lt;/h2&gt;

&lt;p&gt;The edge cases are where this gets interesting.&lt;/p&gt;

&lt;p&gt;Message expiry is one. If your message sits in the pending queue for too long — say, you're offline for 30 days — the server may no longer accept it. The session might have expired, encryption keys may have rotated (WhatsApp uses ephemeral keys via the Signal Protocol), and the queued message simply fails silently or returns an error your app has to handle gracefully.&lt;/p&gt;

&lt;p&gt;Another edge case: editing and deletion. When you delete a message "for everyone," that's a new queued action — a delete event with the target message ID. If you're offline when you tap delete, the delete event queues up. But if the recipient comes online before you do and reads the message before your delete event reaches the server, the delete may still succeed, but they've already seen it. Eventually consistent can mean "eventually deleted, but too late."&lt;/p&gt;

&lt;p&gt;Clock manipulation is another real issue. If a user manually sets their phone clock backwards, device-generated timestamps can look like messages from the past. Servers defend against this with server-side timestamps as authoritative when there's a large discrepancy.&lt;/p&gt;

&lt;h2&gt;
  
  
  DIY: Build It Yourself
&lt;/h2&gt;

&lt;p&gt;Here's a minimal offline-first message queue in TypeScript — the core pattern without the complexity of a full chat app.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import AsyncStorage from '@react-native-async-storage/async-storage';

interface QueuedMessage {
  id: string;
  content: string;
  recipientId: string;
  timestamp: number;
  status: 'pending' | 'sent' | 'failed';
}

async function queueMessage(content: string, recipientId: string): Promise&amp;lt;void&amp;gt; {
  const message: QueuedMessage = {
    id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
    content,
    recipientId,
    timestamp: Date.now(),
    status: 'pending',
  };

  const raw = await AsyncStorage.getItem('pendingMessages');
  const queue: QueuedMessage[] = raw ? JSON.parse(raw) : [];
  queue.push(message);
  await AsyncStorage.setItem('pendingMessages', JSON.stringify(queue));
}

async function flushQueue(sendToServer: (msg: QueuedMessage) =&amp;gt; Promise&amp;lt;boolean&amp;gt;): Promise&amp;lt;void&amp;gt; {
  const raw = await AsyncStorage.getItem('pendingMessages');
  if (!raw) return;

  const queue: QueuedMessage[] = JSON.parse(raw);
  const remaining: QueuedMessage[] = [];

  for (const msg of queue) {
    const success = await sendToServer(msg);
    if (!success) {
      remaining.push({ ...msg, status: 'failed' });
    }
  }

  await AsyncStorage.setItem('pendingMessages', JSON.stringify(remaining));
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;queueMessage&lt;/code&gt; writes to local storage immediately and returns — no network call. &lt;code&gt;flushQueue&lt;/code&gt; is called when connectivity is detected, iterates through pending messages oldest-first, and removes only the ones that succeed. Failed messages stay in the queue for the next retry.&lt;/p&gt;

&lt;p&gt;Notice the ID format: a timestamp plus a random suffix. That's a poor person's globally unique ID, enough for small-scale use. Production systems use proper UUIDs or server-assigned IDs with client-side temporary IDs that get swapped on acknowledgement.&lt;/p&gt;

&lt;p&gt;Try implementing this with a real network listener — the &lt;code&gt;NetInfo&lt;/code&gt; API on React Native fires an event every time connectivity changes. Wire &lt;code&gt;flushQueue&lt;/code&gt; to that event and watch your "pending" messages go live the moment you turn off airplane mode. It is one thing to read about offline queuing, but it is a whole different feeling when you watch your queued messages flush in order the instant the signal comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;If you are thinking of how media is handled on these gigantic platforms, we have already discussed it in the previous blog. You can check it out here:&amp;nbsp;&lt;a href="https://cosmoscribe.hashnode.dev/how-instagram-stores-your-reels-photos-and-drafts-without-losing-a-frame" rel="noopener noreferrer"&gt;How Instagram Stores Your Reels, Photos, and Drafts Without Losing a Frame&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy Exploration!&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>software</category>
      <category>development</category>
    </item>
    <item>
      <title>React Navigation vs Expo Router: Which One Should You Actually Use?</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Mon, 15 Jun 2026 06:44:00 +0000</pubDate>
      <link>https://dev.to/kishanag028/react-navigation-vs-expo-router-which-one-should-you-actually-use-p2e</link>
      <guid>https://dev.to/kishanag028/react-navigation-vs-expo-router-which-one-should-you-actually-use-p2e</guid>
      <description>&lt;p&gt;Liquid syntax error: Variable '{{
          title: "'Feed',"
          tabBarIcon: ({ color }' was not properly terminated with regexp: /\}\}/&lt;/p&gt;
</description>
      <category>appdev</category>
      <category>reactnative</category>
      <category>development</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Architecting Production React Native Apps: How Instagram, WhatsApp, and Uber Think About Scale</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Sun, 14 Jun 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/kishanag028/architecting-production-react-native-apps-how-instagram-whatsapp-and-uber-think-about-scale-42d</link>
      <guid>https://dev.to/kishanag028/architecting-production-react-native-apps-how-instagram-whatsapp-and-uber-think-about-scale-42d</guid>
      <description>&lt;p&gt;Ok, before we go into the depths of these concepts, I want to tell you that we will take it easy. I don't want you to get overwhelmed by the company names in the title. We are not cloning Instagram. We are not rebuilding WhatsApp. We are going to use them as lenses — each one teaches you something specific about how production mobile engineering actually thinks.&lt;/p&gt;

&lt;p&gt;By the end of this, you will have a mental model for structuring React Native applications that can grow without collapsing under their own weight. Whether you are building a solo project or joining a team that already has 40 engineers, this is the thinking you need.&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dhaba Problem
&lt;/h2&gt;

&lt;p&gt;There is a dhaba near my college that made the best chole bhature I have ever eaten. One cook, one kitchen, no menu, no system. The cook just knew where everything was. It worked perfectly.&lt;/p&gt;

&lt;p&gt;Then they opened a second location.&lt;/p&gt;

&lt;p&gt;Everything fell apart. The second cook didn't know the recipe. There was no system to teach it. The original cook couldn't be in two places. A dhaba can survive on one person's tribal knowledge. A chain cannot.&lt;/p&gt;

&lt;p&gt;Most React Native codebases start as dhabas. Everything in one &lt;code&gt;components/&lt;/code&gt; folder. Navigation in &lt;code&gt;App.js&lt;/code&gt;. API calls are scattered across screens. State managed with &lt;code&gt;useState&lt;/code&gt; wherever it's needed. For the first few months, it works perfectly. The original developer knows where everything is.&lt;/p&gt;

&lt;p&gt;Then the team grows. Or the app grows. Or both. Suddenly, nobody knows where anything is, touching one screen breaks three others, and onboarding a new developer takes a week just for them to understand the folder structure.&lt;/p&gt;

&lt;p&gt;This is the problem architecture solves. Not performance — a flat structure performs just fine. Architecture is about &lt;em&gt;how many people can work on this simultaneously without stepping on each other&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature-Based Architecture: Thinking in Districts
&lt;/h2&gt;

&lt;p&gt;The shift you need to make is from &lt;strong&gt;type-based&lt;/strong&gt; to &lt;strong&gt;feature-based&lt;/strong&gt; organization.&lt;/p&gt;

&lt;p&gt;Type-based is the instinct everyone starts with:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;src/
├── components/
├── screens/
├── hooks/
├── utils/
└── services/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Looks clean. Falls apart at 30 screens. Your &lt;code&gt;components/&lt;/code&gt; folder has 80 files. &lt;code&gt;hooks/&lt;/code&gt; has 40. Finding the hook for the feed screen means digging through everything unrelated to it.&lt;/p&gt;

&lt;p&gt;Feature-based reorganizes around product domains:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;src/
├── features/
│   ├── feed/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── api/
│   │   └── store/
│   ├── messaging/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── api/
│   │   └── store/
│   ├── auth/
│   └── profile/
├── shared/
│   ├── components/    ← truly reusable UI only
│   ├── hooks/
│   └── utils/
└── core/
    ├── api/           ← base API client
    ├── navigation/
    └── storage/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Everything that belongs to the feed lives inside &lt;code&gt;features/feed/&lt;/code&gt;. A developer working on messaging never needs to open the feed folder. A developer onboarding to the team can open one feature folder and understand that entire slice of the product.&lt;/p&gt;

&lt;p&gt;The rule is simple: if a file is only used by one feature, it lives inside that feature. If it's used by two or more features, it moves to &lt;code&gt;shared/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0tbe34q2qklqauwu7on.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy0tbe34q2qklqauwu7on.png" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Expo Router at Production Scale
&lt;/h2&gt;

&lt;p&gt;Expo Router's file-based routing maps cleanly onto feature-based architecture. The &lt;code&gt;app/&lt;/code&gt; folder is your navigation layer. The &lt;code&gt;src/features/&lt;/code&gt; folder is your business logic layer. They stay separate.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;├── app/                          ← Navigation (Expo Router)
│   ├── _layout.tsx               ← Root: fonts, providers, theme
│   ├── +not-found.tsx
│   │
│   ├── (auth)/
│   │   ├── _layout.tsx
│   │   ├── login.tsx
│   │   ├── register.tsx
│   │   └── forgot-password.tsx
│   │
│   └── (app)/
│       ├── _layout.tsx           ← Auth guard
│       ├── (tabs)/
│       │   ├── _layout.tsx       ← Tab navigator
│       │   ├── feed.tsx
│       │   ├── explore.tsx
│       │   ├── inbox.tsx
│       │   └── profile.tsx
│       │
│       ├── post/
│       │   ├── [id].tsx
│       │   └── [id]/comments.tsx
│       │
│       ├── chat/
│       │   ├── index.tsx
│       │   └── [conversationId].tsx
│       │
│       └── settings/
│           ├── index.tsx
│           ├── account.tsx
│           └── notifications.tsx
│
└── src/                          ← Business logic (feature-based)
    ├── features/
    ├── shared/
    └── core/&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Your screen files in &lt;code&gt;app/&lt;/code&gt; are thin shells. They import from &lt;code&gt;src/features/&lt;/code&gt; and render. All the logic — data fetching, state, business rules — lives in the feature folder, not in the screen file.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// app/(app)/(tabs)/feed.tsx — thin shell
import { FeedContainer } from '@/features/feed/components/FeedContainer';

export default function FeedScreen() {
  return &amp;lt;FeedContainer /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The screen file is five lines. The complexity lives where it belongs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghlbf1jyv0l6jiogw18q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fghlbf1jyv0l6jiogw18q.png" width="799" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Authentication Architecture
&lt;/h2&gt;

&lt;p&gt;Auth is the first place most apps get complicated. And the first place most developers cut corners, and they regret it later.&lt;/p&gt;

&lt;p&gt;The architecture decision that matters is: where does auth state live, and who is allowed to read it?&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// core/auth/AuthProvider.tsx
import { createContext, useContext, useEffect, useState } from 'react';
import * as SecureStore from 'expo-secure-store';
import { authApi } from './authApi';

type AuthState = {
  user: User | null;
  token: string | null;
  isLoading: boolean;
};

const AuthContext = createContext&amp;lt;AuthState &amp;amp; {
  login: (credentials: Credentials) =&amp;gt; Promise&amp;lt;void&amp;gt;;
  logout: () =&amp;gt; Promise&amp;lt;void&amp;gt;;
}&amp;gt;(null!);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState&amp;lt;AuthState&amp;gt;({
    user: null,
    token: null,
    isLoading: true,
  });

  useEffect(() =&amp;gt; {
    // On mount, check if a token exists in secure storage
    async function loadToken() {
      const token = await SecureStore.getItemAsync('auth_token');
      if (token) {
        const user = await authApi.getMe(token);
        setState({ user, token, isLoading: false });
      } else {
        setState(s =&amp;gt; ({ ...s, isLoading: false }));
      }
    }
    loadToken();
  }, []);

  async function login(credentials: Credentials) {
    const { user, token } = await authApi.login(credentials);
    await SecureStore.setItemAsync('auth_token', token);
    setState({ user, token, isLoading: false });
  }

  async function logout() {
    await SecureStore.deleteItemAsync('auth_token');
    setState({ user: null, token: null, isLoading: false });
  }

  return (
    &amp;lt;AuthContext.Provider value={{ ...state, login, logout }}&amp;gt;
      {children}
    &amp;lt;/AuthContext.Provider&amp;gt;
  );
}

export const useAuth = () =&amp;gt; useContext(AuthContext);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice: the token lives in &lt;code&gt;SecureStore&lt;/code&gt;, not &lt;code&gt;AsyncStorage&lt;/code&gt;. Sensitive credentials belong in the device keychain, not in a plain key-value store.&lt;/p&gt;

&lt;p&gt;The layout-level guard in Expo Router reads from this context:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// app/(app)/_layout.tsx
import { Redirect, Stack } from 'expo-router';
import { useAuth } from '@/core/auth/AuthProvider';

export default function AppLayout() {
  const { user, isLoading } = useAuth();

  if (isLoading) return &amp;lt;SplashScreen /&amp;gt;;
  if (!user) return &amp;lt;Redirect href="/login" /&amp;gt;;

  return &amp;lt;Stack /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One guard, covering every screen under &lt;code&gt;(app)/&lt;/code&gt;. Add a new protected screen — create the file, and it's automatically guarded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnlimrkmih4pgio37xr7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnlimrkmih4pgio37xr7n.png" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  State Management at Scale
&lt;/h2&gt;

&lt;p&gt;This is the question every growing React Native team debates. Redux? Zustand? Jotai? Context? Just React Query?&lt;/p&gt;

&lt;p&gt;The honest answer: it depends on the type of state you are managing. There are three distinct categories, and they need different tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server state&lt;/strong&gt; — data that lives on a server and needs to be fetched, cached, synchronized, and invalidated. This is most of your app's data: user profiles, feed posts, and messages. Use &lt;strong&gt;TanStack Query&lt;/strong&gt; (React Query). It handles caching, background refetching, optimistic updates, and loading/error states. This alone replaces 60% of what most apps use Redux for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client state&lt;/strong&gt; — UI state that doesn't come from the server and doesn't need to persist: which tab is active, whether a modal is open, and the current theme. Use &lt;strong&gt;Zustand&lt;/strong&gt; for the shared client state, &lt;code&gt;useState&lt;/code&gt; for local. Zustand is 1KB, has no boilerplate, and works without a Provider wrapper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent state&lt;/strong&gt; — data that needs to survive app restarts: user preferences, auth tokens, offline cache, draft messages. Use &lt;strong&gt;MMKV&lt;/strong&gt; for fast synchronous key-value storage, &lt;strong&gt;SQLite (via expo-sqlite)&lt;/strong&gt; for structured data like messages and posts.
&lt;pre&gt;&lt;code&gt;// A Zustand store for UI state — no boilerplate, no actions, no reducers
import { create } from 'zustand';&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;code&gt;
&lt;/code&gt;
&lt;/ul&gt;
&lt;code&gt;

&lt;p&gt;type UIStore = {&lt;br&gt;
  activeTab: string;&lt;br&gt;
  isComposerOpen: boolean;&lt;br&gt;
  setActiveTab: (tab: string) =&amp;gt; void;&lt;br&gt;
  toggleComposer: () =&amp;gt; void;&lt;br&gt;
};&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;export const useUIStore = create&amp;lt;UIStore&amp;gt;((set) =&amp;gt; ({&lt;br&gt;
  activeTab: 'feed',&lt;br&gt;
  isComposerOpen: false,&lt;br&gt;
  setActiveTab: (tab) =&amp;gt; set({ activeTab: tab }),&lt;br&gt;
  toggleComposer: () =&amp;gt; set((s) =&amp;gt; ({ isComposerOpen: !s.isComposerOpen })),&lt;br&gt;
}));&lt;/code&gt;&lt;br&gt;
No actions. No reducers. No dispatch. Just a store and setters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule for choosing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If TanStack Query can handle it, use TanStack Query.&lt;/li&gt;
&lt;li&gt;If it's UI state, use Zustand.&lt;/li&gt;
&lt;li&gt;If it needs to persist, use MMKV or SQLite.&lt;/li&gt;
&lt;li&gt;Redux is the right choice when you need complex state machines, time-travel debugging, or your team already has deep Redux expertise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise, the simpler stack wins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd90jttjuh0cm7ao7u7am.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd90jttjuh0cm7ao7u7am.png" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The API Layer: One Client, Not Many
&lt;/h2&gt;

&lt;p&gt;A mistake almost every app makes early on: API calls directly inside components. &lt;code&gt;fetch('https://api.yourapp.com/users')&lt;/code&gt; scattered across 30 different files.&lt;/p&gt;

&lt;p&gt;The problem hits when your base URL changes, you add auth headers, you switch from REST to GraphQL, or you need to add request logging. You make the change in 30 places. You miss one.&lt;/p&gt;

&lt;p&gt;The API layer is a single module that owns all network communication.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// core/api/client.ts
import axios from 'axios';
import { getToken } from '@/core/auth/tokenStorage';

export const apiClient = axios.create({
  baseURL: process.env.EXPO_PUBLIC_API_URL,
  timeout: 10000,
});

// Attach auth token to every request automatically
apiClient.interceptors.request.use(async (config) =&amp;gt; {
  const token = await getToken();
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Handle 401s globally — redirect to login
apiClient.interceptors.response.use(
  (response) =&amp;gt; response,
  async (error) =&amp;gt; {
    if (error.response?.status === 401) {
      await clearToken();
      router.replace('/login');
    }
    return Promise.reject(error);
  }
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Every feature's API module uses this client, not &lt;code&gt;fetch&lt;/code&gt; directly.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// features/feed/api/feedApi.ts
import { apiClient } from '@/core/api/client';
import type { Post } from '../types';

export const feedApi = {
  getPosts: async (page: number): Promise&amp;lt;Post[]&amp;gt; =&amp;gt; {
    const { data } = await apiClient.get('/posts', { params: { page } });
    return data;
  },

  likePost: async (postId: string): Promise&amp;lt;void&amp;gt; =&amp;gt; {
    await apiClient.post(`/posts/${postId}/like`);
  },
};&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Change the base URL once. Add a header once. Log every request once. The features don't care about any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realtime Systems: Chat, Live Updates, and Ride Tracking
&lt;/h2&gt;

&lt;p&gt;This is where architecture gets genuinely interesting. Real-time systems are a different class of problem from standard request-response API calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chat (WhatsApp mental model)
&lt;/h3&gt;

&lt;p&gt;WhatsApp's core problem is not sending messages — that's a POST request. The hard problem is &lt;em&gt;receiving&lt;/em&gt; messages when the app is in the background, displaying them in order, handling failed sends, and syncing across devices.&lt;/p&gt;

&lt;p&gt;The real-time transport for chat is &lt;strong&gt;WebSockets&lt;/strong&gt;. A persistent connection between the client and server that lets the server push messages to the client without the client asking.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// features/messaging/hooks/useMessageSocket.ts
import { useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';

export function useMessageSocket(conversationId: string) {
  const ws = useRef&amp;lt;WebSocket | null&amp;gt;(null);
  const queryClient = useQueryClient();

  useEffect(() =&amp;gt; {
    ws.current = new WebSocket(
      `wss://api.yourapp.com/ws/conversations/${conversationId}`
    );

    ws.current.onmessage = (event) =&amp;gt; {
      const message = JSON.parse(event.data);

      // Push the incoming message into TanStack Query's cache
      // The UI updates automatically — no manual setState
      queryClient.setQueryData(
        ['messages', conversationId],
        (old: Message[] = []) =&amp;gt; [...old, message]
      );
    };

    ws.current.onclose = () =&amp;gt; {
      // Reconnection logic here
    };

    return () =&amp;gt; ws.current?.close();
  }, [conversationId]);
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The incoming message lands directly in TanStack Query's cache. Every component subscribed to &lt;code&gt;['messages', conversationId]&lt;/code&gt; re-renders automatically. No event emitters, no global state, no manual dispatch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live Location (Uber mental model)
&lt;/h3&gt;

&lt;p&gt;Uber's driver location problem is different from chat. Messages can tolerate a two-second delay. A driver's location on a map cannot, the user expects it to move smoothly in near real-time.&lt;/p&gt;

&lt;p&gt;The transport for this is &lt;strong&gt;WebSockets or Server-Sent Events&lt;/strong&gt;, with the client receiving location updates at a high frequency and the map component interpolating between positions for smooth animation.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// features/ride/hooks/useDriverLocation.ts
import { useEffect, useState } from 'react';

type LatLng = { latitude: number; longitude: number };

export function useDriverLocation(rideId: string) {
  const [location, setLocation] = useState&amp;lt;LatLng | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    const ws = new WebSocket(`wss://api.yourapp.com/ws/rides/${rideId}/location`);

    ws.onmessage = (event) =&amp;gt; {
      const { latitude, longitude } = JSON.parse(event.data);
      setLocation({ latitude, longitude });
    };

    return () =&amp;gt; ws.close();
  }, [rideId]);

  return location;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The map component takes this location and animates the driver marker. The WebSocket delivers new coordinates every 1–2 seconds. The map library handles the smooth animation between points.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live Content Updates (Instagram/Netflix mental model)
&lt;/h3&gt;

&lt;p&gt;Instagram's feed updates when someone you follow posts. Netflix showing a download's progress. These don't need WebSockets; the urgency is lower, and maintaining a persistent connection for every user is expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-Sent Events (SSE)&lt;/strong&gt; work here. A one-directional push channel from server to client, lighter than WebSockets, reconnects automatically.&lt;/p&gt;

&lt;p&gt;For lower-frequency updates, &lt;strong&gt;polling with TanStack Query&lt;/strong&gt; is often the right answer. Refetch the feed every 30 seconds. Simple, predictable, no persistent connection cost.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Polling with TanStack Query — Instagram "new posts" pattern
const { data: newPosts } = useQuery({
  queryKey: ['feed', 'new'],
  queryFn: feedApi.getNewPosts,
  refetchInterval: 30_000,       // Every 30 seconds
  refetchIntervalInBackground: false, // Pause when app is backgrounded
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Sounds hard, right? TanStack Query handles all of it in two lines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1wu0encd35yjzfuw76nr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1wu0encd35yjzfuw76nr.png" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline-First Support
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;You might be thinking, "My app uses an API. If there's no internet, there's no data. What is there to cache?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A lot. And this is the difference between an app that feels native and one that feels like a web page in a frame.&lt;/p&gt;

&lt;p&gt;Offline-first means the app renders content from local cache immediately on launch, syncs with the server in the background, and queues writes when there's no connection to replay when connectivity returns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading offline:&lt;/strong&gt; TanStack Query caches responses in memory. For persistence across app restarts, combine it with &lt;strong&gt;MMKV&lt;/strong&gt; as the persistence layer.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// core/api/queryClient.ts
import { QueryClient } from '@tanstack/react-query';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { MMKV } from 'react-native-mmkv';

const storage = new MMKV();

const mmkvStorageAdapter = {
  getItem: (key: string) =&amp;gt; storage.getString(key) ?? null,
  setItem: (key: string, value: string) =&amp;gt; storage.set(key, value),
  removeItem: (key: string) =&amp;gt; storage.delete(key),
};

export const persister = createSyncStoragePersister({
  storage: mmkvStorageAdapter,
});

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24, // Cache for 24 hours
    },
  },
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Writing offline:&lt;/strong&gt; Queue mutations locally when offline, replay them when the connection returns.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Optimistic update with offline queue
const { mutate: likePost } = useMutation({
  mutationFn: feedApi.likePost,
  onMutate: async (postId) =&amp;gt; {
    // Update the cache immediately — the UI feels instant
    await queryClient.cancelQueries({ queryKey: ['posts'] });
    queryClient.setQueryData(['posts'], (old: Post[]) =&amp;gt;
      old.map(p =&amp;gt; p.id === postId ? { ...p, liked: true, likes: p.likes + 1 } : p)
    );
  },
  onError: (err, postId, context) =&amp;gt; {
    // If it fails, roll back
    queryClient.setQueryData(['posts'], context.previousPosts);
  },
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The user taps like. The UI updates instantly. The network request happens in the background. If it fails, the UI rolls back (Optimistic Updates). If the user is offline, TanStack Query's offline mutation queuing holds the request until connectivity returns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwod9lzmmu42si5zqm2g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwod9lzmmu42si5zqm2g.png" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  App Startup Optimization
&lt;/h2&gt;

&lt;p&gt;The two numbers that matter most for a startup: &lt;strong&gt;Time to Interactive (TTI)&lt;/strong&gt; and &lt;strong&gt;perceived launch time,&lt;/strong&gt; like &lt;code&gt;LightHouse WebAnalytics&lt;/code&gt; for web&lt;/p&gt;

&lt;p&gt;TTI is how long before the user can actually use the app. Perceived launch time is how quickly it feels like something is happening, which you can improve even without reducing actual TTI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Defer heavy initialization.&lt;/strong&gt; Don't initialize analytics, crash reporting, push notification handlers, and feature flag SDKs synchronously on startup. Defer them.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// app/_layout.tsx
export default function RootLayout() {
  useEffect(() =&amp;gt; {
    // These run after the first frame is painted, not before
    initAnalytics();
    initCrashReporting();
    registerPushHandlers();
  }, []);

  return &amp;lt;Stack /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;2. Prefetch critical data.&lt;/strong&gt; Before the user navigates anywhere, start fetching the data they will almost certainly need.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Prefetch feed data while auth is being verified
useEffect(() =&amp;gt; {
  queryClient.prefetchQuery({
    queryKey: ['feed'],
    queryFn: feedApi.getPosts,
  });
}, []);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;3. Use a native splash screen until fonts and critical assets are loaded&lt;/strong&gt;, not until all data is fetched. Users accept a splash screen for 500ms. They don't accept a blank white screen for 2 seconds.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import * as SplashScreen from 'expo-splash-screen';
import { useFonts } from 'expo-font';

SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const [fontsLoaded] = useFonts({ 'Inter': require('./assets/Inter.ttf') });

  useEffect(() =&amp;gt; {
    if (fontsLoaded) SplashScreen.hideAsync();
  }, [fontsLoaded]);

  if (!fontsLoaded) return null;

  return &amp;lt;Stack /&amp;gt;;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;4. Lazy load heavy screens.&lt;/strong&gt; React Native's Metro bundler doesn't code-split by default, but you can manually lazy-load screens that users rarely visit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpj9h8ebxyosz7vcrgjb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpj9h8ebxyosz7vcrgjb.png" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Engineering Part
&lt;/h2&gt;

&lt;p&gt;Let me share a very interesting problem my friend faced, where all of this stopped being theory and became urgent. He was working on a consumer app that had grown from 5 screens to 38 over 14 months. The original architecture was type-based — one big &lt;code&gt;components/&lt;/code&gt; folder, API calls in screens, state everywhere. Everything in &lt;code&gt;App.js&lt;/code&gt; navigated to everything else via React Navigation string references.&lt;/p&gt;

&lt;p&gt;At 38 screens and 4 developers, the codebase became genuinely dangerous. Changing the profile screen broke the settings screen because they shared a component that had grown side-effect dependencies on profile-specific state. Nobody knew. There were no boundaries.&lt;/p&gt;

&lt;p&gt;The refactor took three weeks and touched nearly every file. The lesson is not "refactor early", that's too vague. The lesson is: &lt;strong&gt;the cost of architecture is paid once, the cost of no architecture is paid forever.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The specific decisions that matter at scale, and when they matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Feature isolation&lt;/strong&gt; — matters from the first day a second developer joins. Two people in the same folder guarantee merge conflicts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed API client&lt;/strong&gt; — matters from the first time you add auth headers, change an endpoint, or need to mock the API in tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server state separation&lt;/strong&gt; — matters from the first screen that shows data from two different API endpoints. Without TanStack Query, you write loading/error/cache logic by hand, every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite for structured offline data&lt;/strong&gt; — matters when your app has more than a few thousand cacheable records. MMKV is fast, but it stores everything as strings. SQLite gives you queries, indexes, and relational structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSockets for real-time&lt;/strong&gt; — matters the moment your app needs any data to update without user action. Polling works up to a point. WebSockets scale better and feel more alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Catch: When Architecture Becomes the Product
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;You might be thinking, "This is a lot of setup before writing a single feature."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It is. And there is a real trap here.&lt;/p&gt;

&lt;p&gt;Over-architected apps fail just as often as under-architected ones. The difference is who fails: under-architected apps fail when the team tries to scale. Over-architected apps fail when the team is too slow to ship anything before the runway runs out.&lt;/p&gt;

&lt;p&gt;A solo developer building an MVP does not need feature-based architecture, a typed API client, offline sync, and a WebSocket layer. They need to ship. A flat structure, &lt;code&gt;useState&lt;/code&gt; everywhere, and direct &lt;code&gt;fetch&lt;/code&gt; calls is the right architecture for week one.&lt;/p&gt;

&lt;p&gt;The question to ask is: &lt;strong&gt;What is the next forcing function?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Second developer joins → add feature boundaries&lt;/li&gt;
&lt;li&gt;Auth gets complex → extract auth layer&lt;/li&gt;
&lt;li&gt;API changes break multiple screens → add API client abstraction&lt;/li&gt;
&lt;li&gt;Users complain about loading times → add TanStack Query&lt;/li&gt;
&lt;li&gt;Need chat → add WebSockets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecture should lag slightly behind complexity, not anticipate it by six months. Building for Instagram's scale when you have 200 users is not engineering, it's procrastination with better vocabulary.&lt;/p&gt;

&lt;h2&gt;
  
  
  DIY: A Production-Grade Starting Point
&lt;/h2&gt;

&lt;p&gt;Here is a starter structure you can clone for your next serious React Native project. Not a toy. Not a tutorial app. Something you can build a real product on.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;my-app/
│
├── app/                          ← Expo Router (navigation only)
│   ├── _layout.tsx
│   ├── +not-found.tsx
│   ├── (auth)/
│   │   ├── _layout.tsx
│   │   ├── login.tsx
│   │   └── register.tsx
│   └── (app)/
│       ├── _layout.tsx           ← Auth guard here
│       ├── (tabs)/
│       │   ├── _layout.tsx
│       │   ├── index.tsx         ← feed
│       │   ├── explore.tsx
│       │   └── inbox.tsx
│       └── [feature]/
│           └── [id].tsx
│
├── src/
│   ├── features/                 ← Business logic, one folder per domain
│   │   ├── feed/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   │   ├── useFeed.ts
│   │   │   │   └── useLikePost.ts
│   │   │   ├── api/
│   │   │   │   └── feedApi.ts
│   │   │   └── types.ts
│   │   ├── messaging/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   │   ├── useConversations.ts
│   │   │   │   └── useMessageSocket.ts
│   │   │   ├── api/
│   │   │   └── types.ts
│   │   └── auth/
│   │       ├── AuthProvider.tsx
│   │       ├── authApi.ts
│   │       └── tokenStorage.ts
│   │
│   ├── shared/                   ← Used by 2+ features
│   │   ├── components/
│   │   │   ├── Button.tsx
│   │   │   ├── Avatar.tsx
│   │   │   └── EmptyState.tsx
│   │   ├── hooks/
│   │   │   ├── useNetworkStatus.ts
│   │   │   └── useDebounce.ts
│   │   └── utils/
│   │       ├── formatDate.ts
│   │       └── formatCurrency.ts
│   │
│   └── core/                     ← Infrastructure, not business logic
│       ├── api/
│       │   ├── client.ts         ← Axios instance + interceptors
│       │   └── queryClient.ts    ← TanStack Query client + MMKV persister
│       ├── storage/
│       │   └── mmkv.ts
│       └── config/
│           └── env.ts            ← process.env wrappers with types
│
├── assets/
├── constants/
│   └── theme.ts
└── app.json&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The rule that keeps this clean: &lt;code&gt;app/&lt;/code&gt; &lt;strong&gt;imports from&lt;/strong&gt; &lt;code&gt;src/features/&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; &lt;code&gt;src/features/&lt;/code&gt; &lt;strong&gt;imports from&lt;/strong&gt; &lt;code&gt;src/shared/&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;src/core/&lt;/code&gt;&lt;strong&gt;. Nothing imports from&lt;/strong&gt; &lt;code&gt;app/&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; One direction. No circles.&lt;/p&gt;

&lt;p&gt;Try setting this up for your next project. It feels like overhead on day one. By week four, when you add a new feature without touching anything outside its folder, you will understand why it exists.&lt;/p&gt;

&lt;p&gt;Happy Exploration!&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>development</category>
      <category>appdev</category>
    </item>
    <item>
      <title>React's Virtual DOM: How React Decides What (and What Not) to Redraw</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Sat, 13 Jun 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/kishanag028/reacts-virtual-dom-how-react-decides-what-and-what-not-to-redraw-361k</link>
      <guid>https://dev.to/kishanag028/reacts-virtual-dom-how-react-decides-what-and-what-not-to-redraw-361k</guid>
      <description>&lt;p&gt;Ok, before we go into the depths of these concepts, I want to tell you that we will take it easy. I don't want you to get overwhelmed by the jargon. Virtual DOM, reconciliation, diffing, these words sound like they belong in a research paper, not a blog post.&lt;/p&gt;

&lt;p&gt;Here is the thing. Every React developer has heard "React is fast because of the Virtual DOM." And every React developer has nodded along. But if you ask them &lt;em&gt;why&lt;/em&gt; the Virtual DOM makes it fast, most of them go quiet.&lt;/p&gt;

&lt;p&gt;Let's fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Why Touching the DOM Directly Hurts
&lt;/h2&gt;

&lt;p&gt;Think about the last time you had to repaint an entire wall just to fix one scratch near the skirting board.&lt;/p&gt;

&lt;p&gt;That's what browsers do when you manipulate the DOM without strategy. The browser doesn't just change the one element you touched; it recalculates styles, reflows the layout, and repaints everything that might have been affected. One small change, full repaint.&lt;/p&gt;

&lt;p&gt;Now, imagine a user clicks "like" on one post in a feed of 500. Without any optimization, the browser re-renders the whole feed. Sounds like a massive headache, right?&lt;/p&gt;

&lt;p&gt;This is the exact problem the Virtual DOM was built to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real DOM: Why It's So Expensive
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The DOM (Document Object Model) is a tree-like representation of your HTML that browsers maintain internally. Every element is a live node with styles, event listeners, and layout information all attached to it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me explain it in simple words.&lt;/p&gt;

&lt;p&gt;Every direct DOM operation triggers a chain reaction inside the browser. &lt;code&gt;document.getElementById('title').innerText = 'Hello'&lt;/code&gt; sounds innocent. But the browser recalculates styles, re-runs layout for anything affected, and queues a repaint. It is not broken, it is just expensive by design.&lt;/p&gt;

&lt;p&gt;For a static webpage, this is fine. For an app updating 30 times a second, it becomes a serious bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0i6d143ttx4shwchk7de.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0i6d143ttx4shwchk7de.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Virtual DOM: The Architect's Blueprint
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;You might be thinking, "Why not just be more careful about which DOM updates you trigger? Why add an entire extra layer?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fair question. For a small app, maybe you could manage this manually. But the moment you have dozens of components all reacting to user input, server responses, timers, and route changes, tracking what changed and what didn't becomes a full-time job on its own.&lt;/p&gt;

&lt;p&gt;The Virtual DOM hands that job to React.&lt;/p&gt;

&lt;p&gt;Think of an architect before a renovation. They don't tear down walls to figure out what to change. They work on a blueprint first, a lightweight paper representation of the building. Once all the changes are clear on paper, they hand over a precise list to the construction crew. The crew does only what's on that list. Not the whole building.&lt;/p&gt;

&lt;p&gt;The Virtual DOM is React's blueprint. It is a plain JavaScript object that mirrors the structure of your UI, but with none of the expensive browser machinery attached.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// A simplified Virtual DOM node looks something like this
{
  type: 'div',
  props: { className: 'post-card', id: 'post-42' },
  children: [
    {
      type: 'h2',
      props: {},
      children: ['React Virtual DOM Explained']
    },
    {
      type: 'span',
      props: { className: 'likes' },
      children: ['128 likes']
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This is a plain JavaScript object. Creating and comparing these costs almost nothing, no layout engine, no browser API calls, no repaint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8r4a0eqqo52nj81703hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8r4a0eqqo52nj81703hx.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Render
&lt;/h2&gt;

&lt;p&gt;When your React app loads for the first time, React runs your component functions, converts the returned JSX into a Virtual DOM tree, and uses that tree to build and paint the actual Real DOM nodes.&lt;/p&gt;

&lt;p&gt;The first render is always a full build. There's no way around it, you're starting from nothing. The magic kicks in when something &lt;em&gt;changes&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  State or Props Change: Drawing a New Blueprint
&lt;/h2&gt;

&lt;p&gt;When a user clicks a button, submits a form, or new API data arrives, React state or props update.&lt;/p&gt;

&lt;p&gt;React does not immediately reach for the Real DOM. Instead, it re-runs the affected component functions and builds a brand new Virtual DOM tree, a fresh blueprint of what the UI should now look like.&lt;/p&gt;

&lt;p&gt;You now have two blueprints side by side: the old one (what's on screen) and the new one (what it should look like after the change).&lt;/p&gt;

&lt;p&gt;Sounds familiar, right? This is where diffing comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diffing and Reconciliation: The Spot-the-Difference Game
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Reconciliation is the process by which React compares the old Virtual DOM tree with the new one to determine the minimal set of changes required to update the Real DOM.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me explain it in simple words.&lt;/p&gt;

&lt;p&gt;You've played "spot the difference" puzzles, two nearly identical images, five things changed. React plays this game but with JavaScript tree structures.&lt;/p&gt;

&lt;p&gt;It walks through both trees simultaneously, node by node. Where nothing changed, it does nothing. Where something changed, it records the operation: update this text, swap this class, remove this node entirely. At the end, React holds a precise, minimal patch.&lt;/p&gt;

&lt;p&gt;React does this using a few rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Elements of &lt;strong&gt;different types&lt;/strong&gt; (a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; becoming a &lt;code&gt;&amp;lt;section&amp;gt;&lt;/code&gt;) get torn down and rebuilt from scratch.&lt;/li&gt;
&lt;li&gt;Elements of the &lt;strong&gt;same type&lt;/strong&gt; get their props compared; only the changed props get updated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lists&lt;/strong&gt; use &lt;code&gt;key&lt;/code&gt; props to track which items were added, removed, or moved. This is exactly why React warns you when you forget to add keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Insane, right? You write &lt;code&gt;setLikeCount(c =&amp;gt; c + 1)&lt;/code&gt; and React plays spot-the-difference on your entire component tree to figure out the smallest possible set of Real DOM operations to run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsa8fjx9e7h5415s3uz7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsa8fjx9e7h5415s3uz7r.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying the Diff
&lt;/h2&gt;

&lt;p&gt;Once React has the patch, it enters the commit phase, the only moment it actually touches the Real DOM.&lt;/p&gt;

&lt;p&gt;It applies the diff in order, synchronously. Insert this node. Update that attribute. Remove this one. Instead of re-rendering your 500-post feed, React updates the one &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; whose like count changed. The browser recalculates layout for that tiny corner of the screen, not the whole page.&lt;/p&gt;

&lt;p&gt;That's why React apps feel snappy even as they grow complex.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Engineering Part
&lt;/h2&gt;

&lt;p&gt;Here is the full render-to-commit lifecycle, without the hand-waving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger → Render Phase → Commit Phase&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Trigger
&lt;/h3&gt;

&lt;p&gt;A state or props change schedules a re-render. React doesn't process it immediately; it &lt;strong&gt;batches&lt;/strong&gt; multiple updates from the same event handler into a single pass.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// Both updates collapse into ONE re-render, ONE diff, ONE commit
const handleLike = () =&amp;gt; {
  setLikeCount(c =&amp;gt; c + 1);
  setHasLiked(true);
};&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Two state updates. One re-render. React collects them, rebuilds the Virtual DOM once, diffs once, commits once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Render Phase
&lt;/h3&gt;

&lt;p&gt;React calls the component functions. They return JSX, which becomes new Virtual DOM nodes. React builds the new tree, then diffs it against the previous one.&lt;/p&gt;

&lt;p&gt;This phase is &lt;strong&gt;pure&lt;/strong&gt;, no Real DOM contact. If React needs to interrupt this phase (in Concurrent Mode), it can because no real side effects have happened yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commit Phase
&lt;/h3&gt;

&lt;p&gt;React applies the diff to the Real DOM synchronously, in order. After the DOM is updated, it fires your &lt;code&gt;useEffect&lt;/code&gt; hooks.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  // Runs AFTER React has committed all changes to the Real DOM
  document.title = `${likeCount} people liked this`;
}, [likeCount]);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; fires &lt;em&gt;after&lt;/em&gt; commit, not during. This guarantees the DOM is fully updated before your effect runs.&lt;/p&gt;

&lt;p&gt;The three phases: &lt;strong&gt;Render → Diff → Commit&lt;/strong&gt; repeat on every state or props change. Understanding this loop is what separates React developers who write fast apps from those who keep wondering why their app lags.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Catch: When the Virtual DOM Is Not Enough
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;You might be thinking, "If the Virtual DOM handles all this automatically, why do React apps still go slow sometimes?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because the Virtual DOM optimizes &lt;em&gt;Real DOM writes&lt;/em&gt;. It doesn't optimize &lt;em&gt;component execution&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If a parent component re-renders, all its children re-render by default even if their props didn't change. React diffs those children, confirms nothing changed, skips the DOM update. Correct behavior. But you still paid the cost of running every child function and building every child's Virtual DOM nodes.&lt;/p&gt;

&lt;p&gt;In a small app, this is invisible. In a large one, it destroys your frame rate.&lt;/p&gt;

&lt;p&gt;Let me share a very interesting problem I faced where this bit me hard. I was building a theme editor, the kind where users pick brand colors and see a live preview of their UI update in real time. Dragging the hue slider on the color picker was visibly janky. The preview felt like it was running at 10fps.&lt;/p&gt;

&lt;p&gt;I dropped &lt;a href="https://github.com/aidenybai/react-scan" rel="noopener noreferrer"&gt;React Scan&lt;/a&gt; into the project. If you haven't used it, React Scan overlays a colored flash on every component that re-renders, you literally watch your UI light up in real time. The moment I moved the hue slider, the entire page lit up. The color picker was sitting inside a parent that held the theme state. Every &lt;code&gt;onColorChange&lt;/code&gt; fired a &lt;code&gt;setState&lt;/code&gt; on that parent. That parent re-rendered. Every child of that parent, the font selector, the spacing controls, the preview card, the export button, all of them re-rendered on every single mousemove event.&lt;/p&gt;

&lt;p&gt;The Virtual DOM was correctly diffing all of them and making zero DOM changes for most of them. But the render phase was still executing every single one of those component functions, 60 times a second, as the user dragged.&lt;/p&gt;

&lt;p&gt;The fix was two things: &lt;code&gt;React.memo&lt;/code&gt; on every sibling component that didn't consume the color state, and moving the color state down into a smaller subtree closer to where it was actually used. React Scan went quiet. The slider felt instant.&lt;/p&gt;

&lt;p&gt;The Virtual DOM wasn't the problem. The unchecked render phase was. &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt; are the tools you reach for when the render phase is your bottleneck, and React Scan is how you find out you have the problem in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  DIY: Build It Yourself (Minimal Reconciler)
&lt;/h2&gt;

&lt;p&gt;You don't need to build a full React reconciler for this to click. Three small functions are enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A Minimal Virtual DOM Node
&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;// vdom.js
function createElement(type, props = {}, ...children) {
  return { type, props, children: children.flat() };
}

const vNode = createElement(
  'div',
  { id: 'post-card' },
  createElement('h2', {}, 'React Virtual DOM'),
  createElement('span', { className: 'likes' }, '128 likes')
);

console.log(JSON.stringify(vNode, null, 2));&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Plain JavaScript object. No browser API. No paint. Creating a tree like this is essentially free.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A Minimal Differ
&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;// differ.js
function diff(oldNode, newNode) {
  if (!newNode) return { type: 'REMOVE' };
  if (!oldNode) return { type: 'ADD', newNode };
  if (typeof oldNode !== typeof newNode) return { type: 'REPLACE', newNode };
  if (typeof oldNode === 'string') {
    return oldNode !== newNode ? { type: 'TEXT_UPDATE', newNode } : { type: 'NONE' };
  }
  if (oldNode.type !== newNode.type) return { type: 'REPLACE', newNode };

  const propChanges = {};
  const allProps = new Set([
    ...Object.keys(oldNode.props || {}),
    ...Object.keys(newNode.props || {})
  ]);

  for (const key of allProps) {
    if ((oldNode.props || {})[key] !== (newNode.props || {})[key]) {
      propChanges[key] = (newNode.props || {})[key];
    }
  }

  return {
    type: 'UPDATE',
    propChanges,
    childDiffs: diffChildren(oldNode.children || [], newNode.children || [])
  };
}

function diffChildren(oldChildren, newChildren) {
  const length = Math.max(oldChildren.length, newChildren.length);
  const diffs = [];
  for (let i = 0; i &amp;lt; length; i++) {
    diffs.push(diff(oldChildren[i], newChildren[i]));
  }
  return diffs;
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Call &lt;code&gt;diff(oldTree, newTree)&lt;/code&gt; and inspect the output. You'll see &lt;code&gt;NONE&lt;/code&gt; where nothing changed, &lt;code&gt;TEXT_UPDATE&lt;/code&gt; where text changed, &lt;code&gt;REPLACE&lt;/code&gt; where a node type flipped. That object is the patch only what needs work.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Apply the Diff to the Real DOM
&lt;/h3&gt;

&lt;pre&gt;&lt;code&gt;// commit.js
function applyDiff(domNode, patch) {
  if (patch.type === 'NONE') return;

  if (patch.type === 'REMOVE') {
    domNode.parentNode.removeChild(domNode);
    return;
  }

  if (patch.type === 'ADD') {
    domNode.parentNode.appendChild(createDomNode(patch.newNode));
    return;
  }

  if (patch.type === 'REPLACE') {
    domNode.parentNode.replaceChild(createDomNode(patch.newNode), domNode);
    return;
  }

  if (patch.type === 'TEXT_UPDATE') {
    domNode.textContent = patch.newNode;
    return;
  }

  // UPDATE — apply only the changed props
  for (const [key, value] of Object.entries(patch.propChanges)) {
    if (value === undefined) {
      domNode.removeAttribute(key);
    } else {
      domNode.setAttribute(key, value);
    }
  }

  patch.childDiffs.forEach((childPatch, i) =&amp;gt; {
    applyDiff(domNode.childNodes[i], childPatch);
  });
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The commit function does exactly what the diff says, nothing more. A node with no changes? Skipped. One changed attribute? One &lt;code&gt;setAttribute&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;Try implementing this! It is one thing to read about reconciliation, but it is a whole different feeling when you build two trees by hand, run &lt;code&gt;diff()&lt;/code&gt;, and watch it produce a minimal, precise patch for exactly the nodes that changed.&lt;/p&gt;

&lt;p&gt;Happy Exploration!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Authentication Demystified: Every Login Method, Actually Explained</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Fri, 12 Jun 2026 06:31:00 +0000</pubDate>
      <link>https://dev.to/kishanag028/authentication-demystified-every-login-method-actually-explained-2l7m</link>
      <guid>https://dev.to/kishanag028/authentication-demystified-every-login-method-actually-explained-2l7m</guid>
      <description>&lt;p&gt;Before we get overwhelmed by the jargon, let's talk about the actual problem we are solving. At some point, every app needs to answer one question: &lt;em&gt;is this person actually who they say they are?&lt;/em&gt; That is it. Authentication is just the collection of strategies engineers have invented to answer that reliably.&lt;/p&gt;

&lt;p&gt;Think of it the way you would in the real world. You lock your house because you do not want intruders accessing your stuff. Authentication is exactly that, but for your digital resources.&lt;/p&gt;

&lt;p&gt;Every method needs two ingredients: a &lt;strong&gt;unique identifier&lt;/strong&gt; that picks you out from everyone else (a username, an email address, a phone number, a device) and a &lt;strong&gt;secret&lt;/strong&gt; that proves that identifier genuinely belongs to you. Every method in this post is just a different answer to what those two ingredients should be.&lt;/p&gt;

&lt;p&gt;Let's go through all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Basic Authentication: Username and Password&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwuls84xwod83oqpz31i7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwuls84xwod83oqpz31i7.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The classic. You are given a unique username, and you set a password for it. The server saves that password, and when you come back to log in, the server verifies what you entered against what it has stored for your username. Alternatively, and more commonly these days, your email serves as the unique identifier instead of a username. Same idea, same flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt; Easy to implement and set up. You can roll out your own auth for this without too much trouble, and you own the whole flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt; It is too basic and can be bypassed relatively easily. The entire burden of proof is on the user. They are responsible for picking a strong password, not reusing it elsewhere, and not handing it over to a phishing page. Most people do at least one of those things wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Basic Authentication with Two-Step Verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvjzw5lumixmvewhv2ks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuvjzw5lumixmvewhv2ks.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a step up from the above. Here we verify the user twice before confirming what they are claiming to be.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email and password with a verification link&lt;/strong&gt;: The user enters their email and password. Before we grant access, we send them a verification link. The user has to click that link before we let them through to their resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email and password with an OTP on email&lt;/strong&gt;: The same flow as above, just instead of sending a verification link, we send a time-limited OTP. The user has to enter it before it expires.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone number with password and OTP via SMS&lt;/strong&gt;: Same flow as all the above, but this time the unique identifier is the phone number, and the OTP comes to the user's SMS inbox, verifying that they actually have access to that phone number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt; Still relatively easy to implement on your own. The user now needs to have physical access to the identity resource (their inbox or their phone) to verify themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt; Auth can be bypassed using burner emails or prepaid phone numbers, and this can be misused to access your resources without real accountability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. OIDC, or most famously known as OAuth: Let Someone Credible Vouch for You&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftt5yt8ajytikjkf2waji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftt5yt8ajytikjkf2waji.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;OpenID Connect (OIDC) is an authentication protocol built on top of the OAuth 2.0 framework. It allows applications to securely verify a user's identity and retrieve basic profile information (like name and email) without forcing the user to manage and share passwords&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me explain it in simple words. Here we use social providers to vouch for your identity.&lt;/p&gt;

&lt;p&gt;Think of it like applying for a bank loan. The bank does not know you personally, so they ask for a guarantor: someone credible who can vouch that you will pay back what you owe. The guarantor's word stands in for the bank's direct knowledge of you. Critically, the guarantor has to be someone the bank already trusts.&lt;/p&gt;

&lt;p&gt;If you have ever clicked "Sign in with Google," "Continue with Apple," or "Allow this app to access your GitHub," you have used OAuth.&lt;/p&gt;

&lt;p&gt;Here is how the whole thing actually works:&lt;/p&gt;

&lt;p&gt;First, you need credible guarantors. In our case, these are apps that have agreed to act as guarantors for the people coming to your app. Some famous examples are Google, Facebook, GitHub, Apple, and Microsoft. You go to these providers and ask them to be a guarantor for your users. For this, you &lt;strong&gt;register your app's URLs with the provider&lt;/strong&gt;, and in return the provider gives you a &lt;strong&gt;client ID&lt;/strong&gt; and a &lt;strong&gt;client secret&lt;/strong&gt;. This is essentially authentication for your &lt;em&gt;app itself&lt;/em&gt;, so that the provider can verify that the service redirecting users to its login page is actually yours and not someone impersonating you.&lt;/p&gt;

&lt;p&gt;Now, when a user comes to your app and clicks "Sign in with Google," a process begins. Your app reaches out to the provider and says: &lt;em&gt;"Someone wants to log in. Are you willing to vouch for them?"&lt;/em&gt; The provider then checks if it knows this person. If yes, it shows the user a screen that says: &lt;em&gt;"I will act as a guarantor for you with this app. Would you like to continue?"&lt;/em&gt; The user clicks continue.&lt;/p&gt;

&lt;p&gt;Under the hood, two things happen at this point. The provider sends a &lt;strong&gt;challenge&lt;/strong&gt;, which the user completes at a &lt;strong&gt;redirect URI&lt;/strong&gt; you defined during registration. If that is successful, the provider calls the &lt;code&gt;/callback&lt;/code&gt; route you set up in your app and sends back a &lt;strong&gt;token&lt;/strong&gt;. You use that token to fetch the user's details (name, email, profile picture), and then you save whatever you need and issue your own access tokens or sessions based on your auth strategy from there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt; No password to remember for every site. The guarantor handles identity verification. Also, it is easy for the user to try, since they already have an account with these providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt; If the user loses access to their guarantor (locked out of Google, for instance), they lose access to everything they signed into through that provider. Your app knows users only through the intermediary, like a friend-of-a-friend situation, and cannot reach them directly if the connection breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Passwordless Authentication: Magic Links&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2oytnf1g6gjrjfmiy3l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2oytnf1g6gjrjfmiy3l.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
This one is for those who want to really test a user's patience. Personally, I get irritated every time I encounter this flow.&lt;/p&gt;

&lt;p&gt;The idea is to remove passwords entirely. Every time a user wants to log in, you send them a code or a link to their registered email or phone. A link with the code embedded is more common, since it means the user does not have to manually type anything. They just click. Whoever opens that link is authenticated as that user.&lt;/p&gt;

&lt;p&gt;The most important thing to understand here: if your magic link leaks, your account is compromised. The entire security model rests on that link staying private.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt; The user has to have access to their identifier at all times. The moment they lose access to their inbox or phone is the moment they cannot log in, which means you know the person logging in actually controls that resource. Burner emails are much harder to exploit here because the attacker would also need access to the inbox, not just the address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt; User friction. They have to open their email, find the link, and click it every single time they want to log in. If you log in frequently, this gets old fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Passkeys: Your Device Is the Proof&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filxgvjq6wzraynrk518v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filxgvjq6wzraynrk518v.jpg" alt=" " width="799" height="400"&gt;&lt;/a&gt;&lt;br&gt;
Passkeys are the absolute best, if you ask me. The idea is that you yourself, or your device, serves as the identity. Think about how, on the internet, your identity is your IP address or your domain name. Passkeys do something similar: your device or your biometrics act as a unique identifier for verifying who you are.&lt;/p&gt;

&lt;p&gt;How this works is genuinely fascinating if you understand even a little about encryption. So let us deviate from the main topic for a moment, because it is worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encryption&lt;/strong&gt;, in simple terms, is hiding information in such a way that it cannot be read or decoded easily. A famous and very simple example is &lt;strong&gt;ROT13&lt;/strong&gt;, where you rotate each character in the alphabet by 13 positions, a well-known problem in programming circles as well. That is symmetric encryption at its simplest: the same transformation both encodes and decodes.&lt;/p&gt;

&lt;p&gt;Now, encryption is broadly of two types: &lt;strong&gt;symmetric&lt;/strong&gt; and &lt;strong&gt;asymmetric&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Symmetric encryption is easy to picture. Think of a lock and a key. You have one key, and it can both lock and unlock the lock. Same key, both directions.&lt;/p&gt;

&lt;p&gt;Asymmetric encryption changes this. You have a &lt;em&gt;separate&lt;/em&gt; key to lock and a &lt;em&gt;separate&lt;/em&gt; key to unlock. These are called the &lt;strong&gt;private key&lt;/strong&gt; and the &lt;strong&gt;public key&lt;/strong&gt;. As the names suggest, your private key should stay completely private. Never share it with anyone. Your public key can be shared freely with the world. This separation ensures that even if someone intercepts the public key in transit, they cannot do anything with it. They can only lock things with it. They cannot open what was locked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekrgct4601rbci0937xi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekrgct4601rbci0937xi.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;br&gt;
Now let's come back to authentication.&lt;/p&gt;

&lt;p&gt;In passkeys, your device acts as your private key. When you register a passkey with a service, your device generates a public/private key pair. The &lt;strong&gt;public key is sent to the server&lt;/strong&gt; and stored there. The &lt;strong&gt;private key stays locked inside your device&lt;/strong&gt;, stored in the Keychain on iOS and macOS, the Keystore on Android, or the TPM chip on Windows.&lt;/p&gt;

&lt;p&gt;When you come back to log in, the server sends you a &lt;strong&gt;challenge&lt;/strong&gt;: a piece of random data. You sign that challenge using your private key (unlocked by your biometric, either fingerprint or Face ID). The signed response goes back to the server, and the server verifies it using the public key it stored during registration. If the signature matches, your identity is confirmed.&lt;/p&gt;

&lt;p&gt;No password. Nothing that can be phished or stolen in transit. Secure as long as you do not lose your device or compromise your biometrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt; Highly secure. Extremely convenient for the user. One touch and they are in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt; Implementation is on the harder side compared to the other methods above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Authenticator Apps (TOTP)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq45zvhf3z5fa02hpdyea.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq45zvhf3z5fa02hpdyea.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the next level of security. If you have ever tried making your Google account "extra secure," you would have noticed the option to use an authenticator app. What this does is generate a &lt;strong&gt;TOTP (Time-Based One-Time Password)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;TOTP is an algorithm that generates a unique password for each login attempt, using time as one of its parameters. Here is the key detail most explanations miss: it does not use time &lt;em&gt;alone&lt;/em&gt;. During setup, the server generates a &lt;strong&gt;shared secret&lt;/strong&gt; (usually shown as a QR code that your authenticator app scans and stores). From that point forward, both your server and your authenticator app independently run the same mathematical algorithm, using the &lt;strong&gt;shared secret combined with the current timestamp&lt;/strong&gt; divided into 30-second windows, and they arrive at the same six-digit number.&lt;/p&gt;

&lt;p&gt;You enter that number, the server checks that it matches what it computed on its end, and you are in.&lt;/p&gt;

&lt;p&gt;Because both sides compute the code locally using the shared secret, there is nothing travelling over the network to intercept. This is what makes TOTP meaningfully more secure than SMS OTPs. SIM-swapping attacks that can intercept SMS codes simply have no target here.&lt;/p&gt;

&lt;p&gt;Works entirely offline, too. Your authenticator app does not need a cell signal or internet connection to generate the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt; Highly secure. Resistant to interception and SIM-swapping. Works offline. Requires the attacker to have physical access to your device with the app installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt; Requires the user to download a separate third-party app. If the user loses their phone and has not saved backup codes, they are effectively locked out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. SSO: The Enterprise Special&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn573rsqrdg1zvrl5ajco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn573rsqrdg1zvrl5ajco.png" alt=" " width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagine you run a company. For every new hire, that person has to create 50 separate accounts across every internal tool, then request that their account be added to the right team or group for each one so they can actually start working. Isn't that a massive headache? It would take several hours just to get set up on day one.&lt;/p&gt;

&lt;p&gt;This is exactly where SSO, which stands for Single Sign-On, comes in. As the name suggests, you sign in only once. You log into the company's identity provider, and that single authentication gives you access to all the different software and teams you are supposed to have access to. No manual account creation, no requesting access to 50 different tools.&lt;/p&gt;

&lt;p&gt;When someone leaves the company, an admin revokes their access in one place, and they are immediately locked out of everything simultaneously. Onboarding and offboarding that used to take hours now take minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt; Massive productivity boost for employees. Centralized control for IT admins. Clean, auditable access management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disadvantages:&lt;/strong&gt; Complex and expensive to set up correctly. If the SSO provider goes down, nobody can log into anything. It is a deliberate single point of failure, which is why IdP providers invest heavily in availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Engineering Part
&lt;/h2&gt;

&lt;p&gt;Every method above is trading off three things: &lt;strong&gt;security&lt;/strong&gt;, &lt;strong&gt;user experience&lt;/strong&gt;, and &lt;strong&gt;implementation complexity&lt;/strong&gt;. No single method wins on all three, and the right choice depends entirely on your actual threat model.&lt;/p&gt;

&lt;p&gt;Basic auth is the fastest to ship but leaves the most attack surface. OAuth hands identity to a trusted third party, good for both security and UX, but you now have a dependency you do not control. Passkeys are the most secure and the most frictionless once set up, but carry the largest implementation surface. TOTP occupies a useful middle ground for high-security flows without external dependencies.&lt;/p&gt;

&lt;p&gt;The question most engineers skip: &lt;em&gt;what are you actually defending against?&lt;/em&gt; A personal notes app does not need passkeys. A payment flow that skips two-factor authentication is negligent.&lt;/p&gt;

&lt;p&gt;In most production systems, you do not pick one method. You layer them. OAuth for initial sign-in, TOTP as a second factor for sensitive operations, short-lived sessions throughout, SSO if you are building internal tooling. The combinations matter more than any individual choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Catch
&lt;/h2&gt;

&lt;p&gt;A few things that do not fit neatly into the comparison above but matter when you actually build this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OAuth token storage&lt;/strong&gt; is where most implementations go wrong first. Access tokens in &lt;code&gt;localStorage&lt;/code&gt; are readable by any JavaScript on the page, including third-party scripts. Prefer &lt;code&gt;httpOnly&lt;/code&gt; cookies or hold tokens in memory only. Short-lived access tokens with refresh token rotation are not optional in production; they are the baseline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magic link integrity&lt;/strong&gt; has two non-negotiable rules: the token must be single-use (invalidated immediately after the first click), and it must expire even if it is never clicked. Skip either of these, and you are not implementing magic links. You are implementing persistent session tokens that happen to arrive by email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TOTP clock drift&lt;/strong&gt; is a real support ticket waiting to happen. The standard allows a one-window tolerance: the server checks the previous and next 30-second window alongside the current one, but a device with a significantly drifted clock will fail consistently. Always return a useful, specific error message rather than a generic "invalid code."&lt;/p&gt;

&lt;p&gt;In the next post, we will get into the actual implementation: code for handling sessions, issuing JWTs, setting up OAuth with a provider, and wiring up TOTP from scratch. If the theory clicks here, the code will make it permanent.&lt;/p&gt;

&lt;p&gt;Happy Exploration!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>systemdesign</category>
      <category>security</category>
      <category>authentication</category>
    </item>
    <item>
      <title>The Journey of a Request: What Happens Before Your Code Even Runs?</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Wed, 10 Jun 2026 10:30:00 +0000</pubDate>
      <link>https://dev.to/kishanag028/the-journey-of-a-request-what-happens-before-your-code-even-runs-46pp</link>
      <guid>https://dev.to/kishanag028/the-journey-of-a-request-what-happens-before-your-code-even-runs-46pp</guid>
      <description>&lt;p&gt;Okay, before we go into the depths of these concepts, I want to tell you that we will take it easy. I don’t want you to get overwhelmed by the jargon.&lt;/p&gt;

&lt;p&gt;We spend hours arguing about which programming language is the fastest, or how to write the most optimized database query. We debate whether Go is better than Rust, or if Node.js can handle the load.&lt;/p&gt;

&lt;p&gt;But have you ever thought about the gauntlet a user's request has to run through before it even touches your beautiful code?&lt;/p&gt;

&lt;p&gt;Let's break down the layers of the modern web infrastructure. We have CDNs, WAFs, Load Balancers, API Gateways, Internal Reverse Proxies, Rate Limiting, and Input Sanitization.&lt;/p&gt;

&lt;p&gt;Sounds like a massive headache, right? Let's take them one by one.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Content Delivery Network (CDN): The Franchise Model&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The formal definition: A CDN is a network of interconnected servers that speeds up webpage loading for data-heavy applications.&lt;/p&gt;

&lt;p&gt;Sounds boring. Let me explain it in simple words.&lt;/p&gt;

&lt;p&gt;Think of McDonald’s. If McDonald's only cooked burgers in the US, would you wait a month for your food to arrive in India? Of course not. To expand, they open multiple franchises so you can order from the outlet nearest to your house.&lt;/p&gt;

&lt;p&gt;A CDN does exactly this for your application data. When you have users from all over the globe, you don't want to serve them from a single server in Virginia. You want to get as close to them as possible. Services like Cloudflare or AWS CloudFront act as your franchises.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24mfi4rou0nd8yf1kebt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F24mfi4rou0nd8yf1kebt.png" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a user requests an image, a video, or even a static HTML file, the CDN fetches it from your main server once, caches it, and then directly serves it to anyone else in that region who asks for it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;WAF (Web Application Firewall): The Bouncer at the Door&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A WAF protects web applications by filtering and monitoring HTTP traffic. It stops attacks like Cross-Site Scripting (XSS) and SQL Injection.&lt;/p&gt;

&lt;p&gt;Think of it this way: Do you sleep with your front door wide open? Absolutely not. So why should the ports of your application remain open to anyone on the internet?&lt;/p&gt;

&lt;p&gt;A WAF is the bouncer securing those doors. We write specific rules that dictate who the gate will open for and who gets turned away. If a request comes in carrying a malicious payload that looks like a database command, the WAF literally slams the door in its face before your server even knows someone knocked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9lefrnndvq8g8ujiyx9s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9lefrnndvq8g8ujiyx9s.png" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load Balancer: The Traffic Cop&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A load balancer acts as a "traffic cop" sitting in front of your servers, distributing incoming client requests across a group of backend servers.&lt;/p&gt;

&lt;p&gt;This is exactly what it sounds like. Imagine a busy intersection. If everyone tries to go down the same lane, there will be a massive traffic jam. The load balancer looks at your cluster of 10 servers and says, "Okay, Server 1 is busy sweating over a heavy calculation, let's send this new request to Server 2."&lt;/p&gt;

&lt;p&gt;It maximizes speed and ensures high availability. If one server crashes and burns, the traffic cop just routes traffic to the surviving ones. The user never notices a thing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdej9hb7smsvdmc2zwge.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdej9hb7smsvdmc2zwge.png" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;API Gateway vs. Internal Reverse Proxy: The Watchman and the Guide&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you read my last blog on proxies, you know what a Reverse Proxy is. But wait, if we have an API Gateway, why do we need a reverse proxy? Let's clear the confusion.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API Gateway (The Watchman): This is the security guard at the main gate of your residential society. They check your ID, verify if you are authorized to be there (Authentication/Authorization), check if you paid your subscription, and make sure you aren't trying to sneak in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8uq0hxmq0lgjux0wiqbx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8uq0hxmq0lgjux0wiqbx.png" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Internal Reverse Proxy (The Guide): Once the watchman lets you in, you are inside a massive cluster of buildings (microservices). The Load Balancer points to the Reverse Proxy, and the Reverse Proxy looks at your request and says, "Ah, you want user data? Go to building A. You want payment history? Go to building B."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flrzq58d5q1ev05f0jqf3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flrzq58d5q1ev05f0jqf3.png" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rate Limiting: Preventing the Stampede&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rate limiting is a system design technique that restricts the number of requests a user can make to a service within a specific timeframe.&lt;/p&gt;

&lt;p&gt;Why do we do this? Because someone out there might try to overwhelm our services by sending 10,000 requests per second. If we don't stop them, our servers will spend all their CPU power serving this one malicious user, while legitimate users wait in an endless queue. This is known as a Denial of Service (DoS) attack.&lt;/p&gt;

&lt;p&gt;Rate limiting is our safety valve. "You've had your 100 requests for this minute, buddy. Come back later."&lt;/p&gt;

&lt;p&gt;The Real Engineering Part: How do we actually do this? Usually, we just keep a rapid-fire counter in a fast, in-memory database like Redis. Every time a user makes a request, we increment their counter. If the counter hits 101, we return an HTTP 429 (Too Many Requests) error. Simple, but life-saving.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Input Sanitization: The Most Overlooked Lifesaver&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Believe me, this is the most overlooked layer, but it is the absolute most critical one.&lt;/p&gt;

&lt;p&gt;Users lie. Users will send you malicious scripts pretending to be standard text inputs. If you don't clean (sanitize) that input, your system can be entirely compromised.&lt;br&gt;
&lt;code&gt;hackerbotclaw&lt;/code&gt;&lt;br&gt;
What did the bot do? It found repositories with automated workflow files and made Pull Requests where the "branch name" was actually a malicious script. Because the system didn't sanitize the branch name before echoing it out into the terminal, the script executed right there on GitHub's servers. Just like that, the bot gained control.&lt;/p&gt;

&lt;p&gt;Never trust user input. Always sanitize.&lt;/p&gt;

&lt;p&gt;What's Next?&lt;/p&gt;

&lt;p&gt;This was just a bird's-eye view of the gauntlet. In the upcoming articles, we are going to tear each of these layers apart. We will look at the code, write our own rate limiters, and configure our own load balancers.&lt;/p&gt;

&lt;p&gt;Happy Exploration!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>systemdesign</category>
      <category>architecture</category>
      <category>software</category>
    </item>
    <item>
      <title>The Invisible Middleman: Understanding Forward &amp; Reverse Proxies</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Mon, 08 Jun 2026 20:53:43 +0000</pubDate>
      <link>https://dev.to/kishanag028/the-invisible-middleman-understanding-forward-reverse-proxies-5c3a</link>
      <guid>https://dev.to/kishanag028/the-invisible-middleman-understanding-forward-reverse-proxies-5c3a</guid>
      <description>&lt;p&gt;Okay, so today we are going to talk about proxies.&lt;/p&gt;

&lt;p&gt;Sounds familiar, right? I know, I know. We have all been there during our college days, asking a friend to give a "proxy" to help us meet that strict 75% attendance criteria. Yeah, I was part of that game too.&lt;/p&gt;

&lt;p&gt;But putting college hacks aside, we are going to talk about proxies in web development. We use this word all the time, but what does it actually mean to a software developer? Let's break it down so you understand what it truly is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Concept
&lt;/h2&gt;

&lt;p&gt;A proxy, in the simplest terms, is something that helps us identify ourselves as another individual or server. It is a middleman.&lt;/p&gt;

&lt;p&gt;Think about a VPN. What does a VPN do? It protects our IP address. It takes our web request, sends it to a VPN server, and then that server forwards the request to the final website. The VPN acts as a middleman. That is exactly what a proxy does.&lt;/p&gt;

&lt;p&gt;Now, you might be thinking, "Isn't a middleman intercepting my data a bit fishy?"&lt;/p&gt;

&lt;p&gt;It can be! A malicious middleman is literally called a "Man-in-the-Middle" attack. But in web development, we use trusted proxies for internal purposes to help our services communicate securely and efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Engineering: The Header Injection
&lt;/h2&gt;

&lt;p&gt;Let me share a very interesting problem I faced where a proxy saved my life.&lt;br&gt;
&lt;code&gt;hello.anydomain.com&lt;/code&gt;&lt;br&gt;
You might be thinking, "Yeah, very easy, just fetch it from the request."&lt;/p&gt;

&lt;p&gt;The problem was that the hosting service I was using to deploy the app was overwriting the subdomain data before it reached my code. I couldn't get it.&lt;/p&gt;

&lt;p&gt;Enter the proxy.&lt;/p&gt;

&lt;p&gt;I set up a proxy so that before the request hit my main server, the proxy inspected it. It pulled out the subdomain, added a custom HTTP header with that information, and then forwarded the request to my server. It added a tiny bit of latency, but it worked perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Port 80 Problem (Multiple Projects, One Server)
&lt;/h2&gt;

&lt;p&gt;Where else do we use proxies? Recently, I used a single server to deploy six different Go projects.&lt;/p&gt;

&lt;p&gt;Six projects on one server? Yeah, very easy, we can just run them on different local ports (like 3000, 8080, 9000).&lt;br&gt;
&lt;code&gt;amazon.com&lt;/code&gt;&lt;br&gt;
Why? Because web standards dictate that HTTPS requests automatically go to Port 443, and HTTP requests go to Port 80. You only have one Port 443 on your server. So if all traffic comes through one port, how do we route it to six different services?&lt;/p&gt;

&lt;p&gt;We can't do it directly. We need a proxy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7b8hoocraqx4sfmw2mbo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7b8hoocraqx4sfmw2mbo.png" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We put a proxy at the front door listening to Port 443.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a request comes in for Service A, the proxy internally routes it to Port 3000.&lt;/li&gt;
&lt;li&gt;When a request comes for Service B, the proxy routes it to Port 8080.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as a watchman at the entry gate of a residential society, telling visitors where to go and guiding them to the correct buildings. Now, this can sometimes generate confusion, because a real watchman also checks if you are allowed to enter the society (you can't just let a thief in!). When a proxy starts doing those security checks and authentications, it becomes an API Gateway—but we will save that for another discussion!&lt;/p&gt;

&lt;p&gt;You have probably heard of Nginx, Apache, or Caddy. These are basically reverse proxy servers. Can you build your own in Go or Node.js? Yes, it's actually quite easy. But we use these established tools because they are battle-tested and provide incredible customization. Caddy, for instance, even gives you free, automated SSL certificates!&lt;/p&gt;

&lt;h2&gt;
  
  
  Forward vs. Reverse Proxies
&lt;/h2&gt;

&lt;p&gt;To wrap the theory up, proxies mainly fall into two distinct categories:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Forward Proxy
&lt;/h3&gt;

&lt;p&gt;A forward proxy sits in front of the client. It prevents the backend server from seeing who the actual user is. (Your VPN is a forward proxy).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Goal: Hide the client from the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg833kfkr30pz0eco7u43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg833kfkr30pz0eco7u43.png" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Reverse Proxy
&lt;/h3&gt;

&lt;p&gt;A reverse proxy sits in front of the backend. It hides your internal architecture from the client. Nginx, Apache, and Caddy are examples of reverse proxies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Goal: Hide the server from the client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user just sees that all requests are magically handled by one IP address, but behind that reverse proxy, there could be a mammoth cluster of microservices running. The user never knows what is happening behind the curtain.&lt;/p&gt;

&lt;h2&gt;
  
  
  DIY 1: The Supabase Rescue (Build Your Own Proxy)
&lt;/h2&gt;

&lt;p&gt;Want to try building a reverse proxy yourself? Now is the absolute best time to learn, and I'll tell you why.&lt;br&gt;
&lt;code&gt;*.supabase.co&lt;/code&gt;&lt;br&gt;
The rescue mission circulating on X (Twitter)? Reverse Proxies.&lt;/p&gt;

&lt;p&gt;Instead of changing client-side code or asking thousands of users to install VPNs, developers used Cloudflare Workers to act as a reverse proxy. Here is how you can do it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get your domain on Cloudflare and turn on the "Orange Cloud" (Proxy status) on a specific route (like db.yourapp.com).&lt;/li&gt;
&lt;li&gt;Inside Cloudflare Workers, write a simple Node.js or Bun script.&lt;/li&gt;
&lt;li&gt;Add a custom rule: Whenever a request hits db.yourapp.com, your Worker intercepts it, changes the host header behind the scenes, and forwards the traffic to your actual your-project.supabase.co URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To the ISPs, the traffic just looks like it's going to your custom domain via Cloudflare. The block is completely bypassed. It’s a perfect, real-world example of how understanding web infrastructure and proxies can literally save your startup from going dark.&lt;/p&gt;

&lt;h2&gt;
  
  
  DIY 2: Look Under the Hood (Real Code Examples)
&lt;/h2&gt;

&lt;p&gt;I know some of you are curious. You don't just want to read about the theory; you want to see the guts of how this looks in production.&lt;/p&gt;

&lt;p&gt;Here are two stripped-down, real-world examples from my own production environments demonstrating how these proxies are actually built.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Header Injection &amp;amp; CDN Router (Cloudflare Workers)
&lt;/h3&gt;

&lt;p&gt;Remember my story about the hosting service eating my subdomain data? Here is the exact architecture of how I solved it using a Cloudflare Worker as an Edge Proxy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F78919m3u92nn127fqkbr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F78919m3u92nn127fqkbr.png" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;X-Subdomain&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {&lt;br&gt;
  async fetch(request) {&lt;br&gt;
    const url = new URL(request.url);&lt;br&gt;
    const subdomain = url.hostname.split(".")[0];

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Smart Routing based on Hostname
if (url.hostname === "cdn.letshost.dpdns.org") {
  return await handleCDNRequest(request, url);
} else if (url.hostname === "www.letshost.dpdns.org") {
  return Response.redirect("https://letshost.dpdns.org", 302);
} else {
  // The Header Injection for the main backend
  const proxyUrl = `${process.env.BACKEND_URL}${url.pathname}`;
  const modifiedRequest = new Request(proxyUrl, {
    method: request.method,
    headers: request.headers,
    body: request.body,
    redirect: "manual",
  });

  modifiedRequest.headers.set("X-Subdomain", subdomain);
  return fetch(modifiedRequest);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;},&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;// Edge Router Logic for CDN&lt;br&gt;
async function handleCDNRequest(request, url) {&lt;br&gt;
  let targetUrl;&lt;br&gt;
  const pathWithoutQuery = url.pathname;&lt;br&gt;
  const fileExtension = pathWithoutQuery.split(".").pop().toLowerCase();&lt;/p&gt;

&lt;p&gt;// Route Media to Transformers or Direct Storage based on extension&lt;br&gt;
  if (["mp4", "webm", "avi", "mov", "mkv", "flv", "m4v", "jpg", "jpeg", "png", "gif", "webp", "svg"].includes(fileExtension)) {&lt;br&gt;
    if (url.search) {&lt;br&gt;
      targetUrl = &lt;code&gt;${process.env.TRANSFORMER_URL}${url.pathname}${url.search}&lt;/code&gt;;&lt;br&gt;
    } else {&lt;br&gt;
      targetUrl = &lt;code&gt;${process.env.IMAGE_STORAGE_URL}${url.pathname}&lt;/code&gt;;&lt;br&gt;
    }&lt;br&gt;
  } else {&lt;br&gt;
    targetUrl = &lt;code&gt;${process.env.FILE_STORAGE_URL}${url.pathname}${url.search || ""}&lt;/code&gt;;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;try {&lt;br&gt;
    const proxyRequest = new Request(targetUrl, {&lt;br&gt;
      method: request.method,&lt;br&gt;
      headers: request.headers,&lt;br&gt;
      body: request.body,&lt;br&gt;
      redirect: "manual",&lt;br&gt;
    });&lt;br&gt;
    proxyRequest.headers.delete("Host"); // Prevent host header conflicts&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await fetch(proxyRequest);
return new Response(response.body, {
  status: response.status,
  statusText: response.statusText,
  headers: response.headers,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;} catch (error) {&lt;br&gt;&lt;br&gt;
    return new Response(&lt;code&gt;Proxy Error: ${error.message}&lt;/code&gt;, { status: 500 });&lt;br&gt;&lt;br&gt;
  }&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;


&lt;h3&gt;
  
  
  2. The Port Multiplexing Router (Caddy)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Caddyfile&lt;/code&gt;&lt;code&gt;7999&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:7999 {&lt;br&gt;
    # 1. Clean up trailing slashes&lt;br&gt;
    redir /avatars     /avatars/&lt;br&gt;
    redir /calendars   /calendars/&lt;br&gt;
    redir /charts      /charts/&lt;br&gt;
    redir /github      /github/&lt;br&gt;
    redir /institutions /institutions/&lt;br&gt;
    redir /locations   /locations/&lt;br&gt;
    redir /npm         /npm/

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 2. Serve Static Frontend
handle / {
    header Content-Type text/html
    file_server {
        root ./public
        index index.html
    }
}

# 3. Multiplexing microservices to different local ports
handle_path /avatars/* {
    reverse_proxy localhost:8000
}

handle_path /calendars/* {
    reverse_proxy localhost:8001
}

handle_path /charts/* {
    reverse_proxy localhost:8002
}

handle_path /github/* {
    reverse_proxy localhost:8003
}

handle_path /institutions/* {
    reverse_proxy localhost:8004
}

handle_path /locations/* {
    reverse_proxy localhost:8005
}

handle_path /npm/* {
    reverse_proxy localhost:8006
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;


&lt;h3&gt;
  
  
  3. The CORS Killer (Frontend Development Proxy)
&lt;/h3&gt;

&lt;p&gt;If you write frontend code (React, Vue, Svelte), you know the absolute nightmare that is the CORS error.&lt;br&gt;
&lt;code&gt;localhost:5173&lt;/code&gt;&lt;br&gt;
You could configure your backend to allow all CORS requests, but that can be a security risk if it accidentally ships to production. The smarter, cleaner fix? A Frontend Dev Proxy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fukzog87z737tyztqbu6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fukzog87z737tyztqbu6c.png" width="799" height="436"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;vite.config.js&lt;/code&gt;&lt;br&gt;
JavaScript&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      // Any request starting with /api will be intercepted
      '/api': {
        target: 'http://localhost:8000', // Your backend URL
        changeOrigin: true,
        // Rewrite the URL: remove '/api' before sending to the backend
        rewrite: (path) =&amp;gt; path.replace(/^\/api/, ''),
      },
    },
  },
});&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;How it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your frontend code, you just make a request to /api/users.&lt;/li&gt;
&lt;li&gt;The browser allows it without any CORS errors because it thinks it is talking to the frontend server (localhost:5173/api/users).&lt;/li&gt;
&lt;li&gt;But under the hood, the Vite development server intercepts that request, strips away the /api part, and silently forwards it to your actual backend at &lt;a href="http://localhost:8000/users" rel="noopener noreferrer"&gt;http://localhost:8000/users&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;This is useful and is definitely used, but please be mindful that it's always better to handle CORS on the backend rather than on the frontend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Try implementing these! It is one thing to read about proxies passing data around, but it is a whole different feeling when you write that routing logic and see your custom headers hit your backend perfectly.&lt;/p&gt;

&lt;p&gt;Happy Exploration!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>systemdesign</category>
      <category>devops</category>
    </item>
    <item>
      <title>How Instagram Stores Your Reels, Photos, and Drafts Without Losing a Frame</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Sun, 31 May 2026 11:47:50 +0000</pubDate>
      <link>https://dev.to/kishanag028/how-instagram-stores-your-reels-photos-and-drafts-without-losing-a-frame-aie</link>
      <guid>https://dev.to/kishanag028/how-instagram-stores-your-reels-photos-and-drafts-without-losing-a-frame-aie</guid>
      <description>&lt;p&gt;Ok, before we go into the depths of these concepts, I want to tell you that we will take it easy. I don't want you to get overwhelmed by terms like &lt;strong&gt;pre-signed URLs&lt;/strong&gt;, &lt;strong&gt;object storage&lt;/strong&gt;, &lt;strong&gt;chunked upload pipelines&lt;/strong&gt;, or &lt;strong&gt;CDN edge nodes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We are going to explore all of this through the lens of how a web developer would understand it. If you are coming from a web background and stepping into mobile for the first time, this transition introduces a completely new domain — and covering it from that angle will make it click much faster.&lt;/p&gt;

&lt;p&gt;You've probably saved a Reel draft at 2 AM, opened Instagram four hours later, and it was right there — untouched. Or you've watched a Reel, swiped away, came back, and it loaded instantly without buffering. None of that happens by accident.&lt;/p&gt;

&lt;p&gt;The system behind it is thoughtful, layered, and surprisingly easy to reason about once you see the full picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Starts With a Familiar Problem: Pre-Signed URLs
&lt;/h2&gt;

&lt;p&gt;If you have ever built a frontend with your own backend, you've almost certainly encountered this pattern.&lt;/p&gt;

&lt;p&gt;When a user uploads a file, say, a profile picture or a video, you don't want to route that file through your own backend server. Instead, you generate a &lt;strong&gt;pre-signed URL&lt;/strong&gt;: a link signed by your private key, valid strictly for the purpose you assigned to it, and handed directly to the client. The client uploads straight to a storage provider like AWS S3. Your backend never touches the file.&lt;/p&gt;

&lt;p&gt;This keeps your servers fast and your upload costs low. The storage provider handles the heavy lifting.&lt;/p&gt;

&lt;p&gt;But here's the catch. The moment you hand that URL to the client, you lose visibility. You no longer control the process. Did the upload succeed? Did it fail at 60%? You have no idea.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;webhooks&lt;/strong&gt; come in.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Think of a webhook as a system that says, "Let me know when the job is done."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me explain it in simple words. You expose a secure endpoint on your backend. Once the upload completes, AWS S3 calls that endpoint automatically, saying "job done", ping. You didn't watch the upload happen. You just got notified when it was over.&lt;/p&gt;

&lt;p&gt;Web developers solve this on the browser side easily enough. The browser temporarily holds the file in memory. If a page refresh happens, sure, the data is gone, but for the duration of a normal upload session, the browser manages it just fine.&lt;/p&gt;

&lt;p&gt;Now take that same problem to mobile. And suddenly everything gets harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mobile Upload Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;On mobile, starting a direct upload means the media sits in memory. That's it, just memory.&lt;/p&gt;

&lt;p&gt;Networks drop. Apps close. The OS kills background processes when it needs resources. Imagine a user uploading a 3-minute video. It reaches 98%, and then the connection drops or the app refreshes. Back to 0%. They wait for 100% all over again.&lt;/p&gt;

&lt;p&gt;That is a terrible user experience. And it is entirely avoidable.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You might be thinking, "Can't we just retry the upload automatically in the background?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can. But if the media only lives in memory, there is nothing left to retry after the app closes. The file is gone. Retrying from zero is the only option. The fix has to happen earlier, the moment the upload begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing to the File System First
&lt;/h2&gt;

&lt;p&gt;The moment a user starts an upload, we immediately write the file to a local directory.&lt;/p&gt;

&lt;p&gt;On mobile, tools like &lt;strong&gt;Expo File System&lt;/strong&gt; give apps access to a sandboxed environment on the user's device — a real file system with real directories. The app can read and write files there freely, independent of what the network is doing.&lt;/p&gt;

&lt;p&gt;There are generally two types of directories in this sandbox. A &lt;strong&gt;temporary directory&lt;/strong&gt; for short-lived data and a &lt;strong&gt;persistent data directory&lt;/strong&gt; for things that must survive app restarts, OS cleanup, or low-storage pressure.&lt;/p&gt;

&lt;p&gt;Since we only need the media until the upload finishes, the temporary directory is the right call here. The moment the user initiates the upload, we write the file into temp storage. The user sees the upload progress as normal. Behind the scenes, we've created our own local backup, and now we have something to resume from if anything goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd6plozzebial4oxf6i8q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd6plozzebial4oxf6i8q.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Chunking and Streaming: Why Not Just Upload the Whole File?
&lt;/h2&gt;

&lt;p&gt;Once the file is written locally, we take the storage URL, divide the file into &lt;strong&gt;chunks&lt;/strong&gt;, and stream them one by one.&lt;/p&gt;

&lt;p&gt;Chunking does two important things.&lt;/p&gt;

&lt;p&gt;First, it handles &lt;strong&gt;memory efficiently&lt;/strong&gt;. The entire media file never loads into RAM at once. A 500MB video doesn't need 500MB of device memory to upload; each chunk gets loaded, sent, and released.&lt;/p&gt;

&lt;p&gt;Second, it enables &lt;strong&gt;background processing&lt;/strong&gt;. Because we're streaming chunks rather than sending one massive request, the app can hand the task off to a background process. The UI stays responsive. The app doesn't feel laggy. And if a chunk fails, only that chunk retries — not the entire file.&lt;/p&gt;

&lt;p&gt;This is why you've seen a progress bar on Instagram pause and then quietly resume. The upload wasn't frozen. It was retrying one chunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Storage Comparison: Web vs Mobile
&lt;/h2&gt;

&lt;p&gt;If this pattern feels familiar, it should. Web developers have been solving the same problem in the browser for years.&lt;/p&gt;

&lt;p&gt;On the web, &lt;strong&gt;Local Storage&lt;/strong&gt; handles quick key-value data, but it caps at around 5MB. For anything larger, there's &lt;strong&gt;IndexedDB&lt;/strong&gt;, which offers a much bigger capacity. More recently, browsers added &lt;strong&gt;OPFS&lt;/strong&gt; (Origin Private File System), which essentially runs a sandboxed file system through WebAssembly, structured, persistent, and purpose-built for larger data.&lt;/p&gt;

&lt;p&gt;Mobile mirrors this hierarchy exactly. &lt;strong&gt;Async Storage&lt;/strong&gt; on React Native is the direct equivalent of Local Storage, with the same ~5MB ceiling and the same "never put media here" rule. For real files, Expo File System is your IndexedDB — a sandboxed directory that can handle actual binary data.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You might be thinking, "Why not just throw everything into a SQLite database on the device?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because you never store blobs, i.e. media files, in an SQL database. Technically possible. Never recommended. SQL databases are for structured relational data. Blob storage belongs in systems built for it: file systems, object storage, CDNs. The moment you start storing video frames in SQLite rows, you have a performance problem and a maintenance nightmare.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhqviiwypbac8vho3mnae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhqviiwypbac8vho3mnae.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When You Save a Draft
&lt;/h2&gt;

&lt;p&gt;Whenever a user saves a draft, like the "Save as Draft" option on Instagram Reels or LinkedIn, the app grabs all the serialized data: video frames, audio, filter state, caption text, edit history. It writes everything to a structured location in local storage and leaves it there.&lt;/p&gt;

&lt;p&gt;Think of a photographer you've hired to take your pictures at a wedding. They don't back up every single photo to Google Drive between shots. They capture everything on their memory card first, stay focused on the job, and deal with the backup later. Losing Wi-Fi mid-shoot doesn't cost them a single frame.&lt;/p&gt;

&lt;p&gt;Instagram works the same way. Capture locally first. Upload when the time is right.&lt;/p&gt;

&lt;p&gt;Draft data survives app restarts because the OS writes it to disk, not just to memory. This is also why, when you check "App Info" on your phone, you see the initial install size versus the much larger current size. That difference is data; it is split between things like cache storage and app storage. Everything the app is quietly managing on your behalf.&lt;/p&gt;

&lt;p&gt;Sounds simple, right? But there's a subtle problem: if the user clears the app's cache, or if the OS decides to reclaim storage under pressure, drafts can disappear. This is exactly why heavy apps often warn you before clearing the cache or quietly sync draft metadata to their servers even before you tap publish.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4ivzm9t52xbzsmfifun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl4ivzm9t52xbzsmfifun.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud Storage — Where Your Media Lives After Upload
&lt;/h2&gt;

&lt;p&gt;Once you hit Share, Instagram's servers take over.&lt;/p&gt;

&lt;p&gt;Instagram doesn't store your media in a regular file system like your laptop's hard drive. They use &lt;strong&gt;object storage&lt;/strong&gt; — a system where every photo or video becomes a single blob of data with a unique identifier.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Object storage&lt;/strong&gt; is a data storage architecture where data is managed as discrete units called "objects," each with its own ID, metadata, and content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This gives Instagram the flexibility to store billions of photos across countless servers and retrieve any one of them in milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Engineering Part-Backend handling of uploads
&lt;/h2&gt;

&lt;p&gt;Here is the portion that almost stays the same for web or mobile apps, and you could follow if you are ever dealing with systems involving media&lt;/p&gt;

&lt;p&gt;When your Reel lands on Instagram's servers, it goes through a &lt;strong&gt;media processing pipeline&lt;/strong&gt; before anyone else can watch it.&lt;/p&gt;

&lt;p&gt;Here's what that pipeline does:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transcoding&lt;/strong&gt;: Your phone records in whatever format it supports — H.264, H.265, HEVC. Instagram converts it into multiple output formats and multiple resolutions. One source video becomes dozens of versions: 1080p, 720p, 480p, and lower for &lt;strong&gt;HLS streaming&lt;/strong&gt; ( i.e. the player picks the right one based on the viewer's connection speed at the time of playback ). In this stage, the most commonly used library is &lt;strong&gt;ffmpeg,&lt;/strong&gt; which is the standard for these conversions and the most notorious of all to handle on the server&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thumbnail generation&lt;/strong&gt;: The system extracts the first frame (or a smart-selected moment) and generates a low-resolution preview image. As you scroll your feed, thumbnails just keep appearing instantly, so you don't have to wait. That's because object storage extracts the first frame at processing time, generates a low-res photo, and stores it directly in cache. The feed serves it from there. No network call per thumbnail. Instead, batch calls happen all at once, the data gets saved, and it sits ready for whenever you scroll to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio processing&lt;/strong&gt;: Background music rights checks, audio normalization, and sync validation all happen here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata extraction&lt;/strong&gt;: Duration, aspect ratio, codec — all indexed so the content delivery layer knows exactly what it is about to serve.&lt;/p&gt;

&lt;p&gt;The whole pipeline runs asynchronously. You tap Share, get a "processing" confirmation, and Instagram's servers work through it without blocking your session.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4ahd11jeyidkzoe9t6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4ahd11jeyidkzoe9t6u.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Delivery Using CDNs
&lt;/h2&gt;

&lt;p&gt;Once the pipeline finishes, Instagram pushes the final video files to a &lt;strong&gt;CDN&lt;/strong&gt; — a Content Delivery Network.&lt;/p&gt;

&lt;p&gt;A CDN is a distributed network of servers placed physically close to users around the world. When someone in Bengaluru watches your Reel, they are not fetching that video from a data center in Virginia. They fetch it from the nearest CDN edge server — which might sit in Mumbai.&lt;/p&gt;

&lt;p&gt;I have already covered CDN in a previous blog, you can check it out here: &lt;a href="https://cosmoscribe.hashnode.dev/the-journey-of-a-request-what-happens-before-your-code-even-runs" rel="noopener noreferrer"&gt;CDNs&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching — The Secret Behind Instant Loads
&lt;/h2&gt;

&lt;p&gt;CDN caching handles content at the network level. But caching also happens inside the app itself.&lt;/p&gt;

&lt;p&gt;When you scroll your feed, Instagram prefetches the next few videos before you reach them. The app estimates your scroll speed, predicts which Reels you'll hit next, and starts downloading them in the background. By the time you arrive at that Reel, the app already has it sitting in memory.&lt;/p&gt;

&lt;p&gt;This prefetched data lives in an &lt;strong&gt;in-memory cache,&lt;/strong&gt; which is fast to access, and is gone when you close the app. A second layer, the &lt;strong&gt;disk cache&lt;/strong&gt;, keeps recently viewed content around a bit longer so reopening the app feels instant.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You might be thinking, "Won't caching everything eat up my phone storage?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It does, which is why the cache has a size limit. Instagram's app typically caps its disk cache at a few hundred MBs. When that limit hits, the oldest cached files get evicted first. The content always remains available from the CDN — the local cache is just a shortcut that saves a network round-trip.&lt;/p&gt;

&lt;h3&gt;
  
  
  DIY: Build It Yourself
&lt;/h3&gt;

&lt;p&gt;Here is a simplified yet production-ready version of the local-write-then-chunk-upload pattern in a React Native app, using the Expo File System and a pre-signed URL from S3.&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="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;FileSystem&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expo-file-system&lt;/span&gt;&lt;span class="dl"&gt;"&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;CHUNK_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 5MB per chunk&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;writeToTempAndUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;localUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;uploadId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getPresignedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 1: Copy to temp directory for resilience&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tempPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;FileSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cacheDirectory&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;upload_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;uploadId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.mp4`&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;FileSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tempPath&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;fileInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInfoAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;totalSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileInfo&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;FileSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FileInfo&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;size&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;totalChunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&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;uploadedETags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;totalChunks&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&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="nx"&gt;partNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&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;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Step 2: Get a fresh pre-signed URL per part&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;presignedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPresignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;partNumber&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Step 3: Upload this chunk&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;presignedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;httpMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&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="s2"&gt;Content-Type&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="s2"&gt;video/mp4&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="s2"&gt;Content-Range&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`bytes &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;end&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="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;uploadType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FileSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FileSystemUploadType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BINARY_CONTENT&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;206&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Part &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;partNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;eTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ETag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;uploadedETags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eTag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Part &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;partNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalChunks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; done — ETag: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eTag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 4: Signal completion to your backend (it finalizes the S3 multipart upload)&lt;/span&gt;
  &lt;span class="k"&gt;await&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="s2"&gt;/api/complete-upload&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="s2"&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;headers&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="s2"&gt;Content-Type&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="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;uploadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uploadedETags&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 5: Clean up temp file after success&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FileSystem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;idempotent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cacheDirectory&lt;/code&gt; here is your temp storage — it survives app restarts but can be reclaimed by the OS. The copy step in Step 1 is what buys you resilience. If the upload fails at chunk 3, the full file is still sitting at &lt;code&gt;tempPath&lt;/code&gt;. Only that chunk needs to retry.&lt;/p&gt;

&lt;p&gt;Notice that each part returns an ETag. S3's multipart upload system uses these to verify and reassemble all parts in the correct order when your backend calls &lt;code&gt;CompleteMultipartUpload&lt;/code&gt;. You aren't uploading blind — every chunk is tracked and acknowledged server-side.&lt;/p&gt;

&lt;p&gt;Try implementing this yourself! It is one thing to read about chunked uploads, but it is a whole different feeling when you watch a progress bar actually pause, retry a single chunk, and keep climbing.&lt;/p&gt;

&lt;p&gt;With all this discussion, I hope you had a better understanding of how the uploads on devices behave differently from the web. The backend handling is almost the same for web or mobile.&lt;br&gt;&lt;br&gt;
Happy Exploration!&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>programming</category>
      <category>architecture</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Cuckoo Filters vs. Bloom Filters: Let’s Take it Easy</title>
      <dc:creator>Kishan Agarwal </dc:creator>
      <pubDate>Sat, 30 May 2026 07:55:26 +0000</pubDate>
      <link>https://dev.to/kishanag028/cuckoo-filters-vs-bloom-filters-lets-take-it-easy-3e73</link>
      <guid>https://dev.to/kishanag028/cuckoo-filters-vs-bloom-filters-lets-take-it-easy-3e73</guid>
      <description>&lt;p&gt;Ok, before we go into the depths of these concepts, I want to tell you that we will take it easy. I don’t want you to get overwhelmed about the topics and the depth of the discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bloom Filters
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A Bloom filter is a space-efficient probabilistic data structure that is used to test whether an element is a member of a set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sounds hard, right? In just one sentence: "probabilistic," then "data structure," and all this is what the definition says. Let's break it down so you understand what it truly is.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;Ok, I give you a problem. Think of it for a second. Let's say you have an array of characters (English alphabet characters) of size &lt;em&gt;n,&lt;/em&gt; and you have to find whether a char exists or not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Approach 1 (Loop):&lt;/strong&gt; Quite simple, we can just loop the array once. This gives us a time complexity of O(n)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Approach 2 (Sort + Binary Search):&lt;/strong&gt; We sort the array (O(nlog n)) and then perform a binary search (O(log n)). Steps increased, but at least we reduced the time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Approach 3 (HashMap):&lt;/strong&gt; We store it in a HashMap. This way we take double space, but at least we can optimise the time to O(1)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Can we optimise it further? For most of us, this may be ok because you have to prioritise over time and space. Now, for those who want both, the discussion is for you, my friend.&lt;/p&gt;

&lt;p&gt;Think of this: Have you read about ASCII values before?&lt;/p&gt;

&lt;p&gt;Let's say a user gives me input &lt;strong&gt;'a'&lt;/strong&gt;. I don't store 'a'. Instead, I calculate its relative position from 'a' and store it in a bit array of length 26.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We get a number, consider it as an index.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Store &lt;strong&gt;1&lt;/strong&gt; in the position, else &lt;strong&gt;0&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, to check for a char, we just repeat the process. We can access this in, and the space it will take is just 26 bits. Insane, right?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Engineering Part (The Hashing Reality)
&lt;/h3&gt;

&lt;p&gt;But then, that's for a single char. For a string, we can't do that. And Bloom filters are mostly used on strings. So now get to the real engineering part.&lt;/p&gt;

&lt;p&gt;In Bloom filters, we use a hashing function known as &lt;strong&gt;MurmurHash&lt;/strong&gt;. What is hashing? Hashing is a technique of converting a string to an equivalent number.&lt;/p&gt;

&lt;p&gt;But here is the catch: Hashing is deterministic (Input A always gives Output A), but the universe of strings is infinite, while our array is finite.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For &lt;strong&gt;A,&lt;/strong&gt; you get a hash.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Then &lt;strong&gt;B&lt;/strong&gt; gets a hash.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Then &lt;strong&gt;C&lt;/strong&gt; can also generate the same hash as &lt;strong&gt;B&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Can you see what is happening? These are &lt;strong&gt;collisions&lt;/strong&gt;. The main problem is this: hashing can sometimes give the same index for two strings. So in order to prevent this, we need to be clever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3i5cwp6vhygejaj3ktzq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3i5cwp6vhygejaj3ktzq.jpeg" alt=" " width="800" height="1072"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bloom Filters in Production (Making it Robust)
&lt;/h2&gt;

&lt;p&gt;Now, you might be thinking, &lt;em&gt;"If collisions are inevitable, isn't this useless?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In a real production setup, we don't rely on just one hash function. That would be too risky. In the real world, we use &lt;strong&gt;Multiple Hash Functions&lt;/strong&gt; (let's say functions).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When a string comes in, we run it through Hash Function 1, Hash Function 2, and Hash Function 3.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We get 3 different indexes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We set all 3 positions to &lt;strong&gt;1&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, when we query:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We check all 3 positions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If &lt;strong&gt;all&lt;/strong&gt; of them are 1, then we say &lt;strong&gt;"Yes, it's probably here."&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If &lt;strong&gt;even one&lt;/strong&gt; of them is 0, we can say with &lt;strong&gt;100% guarantee: "It is not here."&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This drastically reduces the false positive rate. It’s like asking three different friends if a restaurant is open. If all three say yes, it's probably open. If one says, "No, I'm standing in front of it, and it's closed," then you know for sure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9rmk8lx1h7ki8ail5xw.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9rmk8lx1h7ki8ail5xw.jpeg" alt=" " width="800" height="1072"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Major Problem
&lt;/h3&gt;

&lt;p&gt;Now, can you see a major problem in this Bloom filter? I also recently came to know about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem is deletion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once a Bloom filter gets filled, let's say User A deletes their account. Technically, this username is free. But your Bloom filter will still say that the username exists.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We can query in O(1)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We can insert in O(1)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;But we cannot delete data.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why? If I go to those 3 positions and set them back to 0, maybe User B was also using one of those spots! If I delete User A, I might accidentally "corrupt" User B. This is why deletion is extremely challenging in Bloom filters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzoovojxwzls2fnfileo.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzoovojxwzls2fnfileo.jpeg" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  So, is there a fix? (The Counting Bloom Filter)
&lt;/h3&gt;

&lt;p&gt;Now you might be thinking, &lt;em&gt;"Can’t we just simply count the items instead of just flipping bits?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Actually, yes. There is something called a &lt;strong&gt;Counting Bloom Filter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The logic is super simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In a normal Bloom filter, we have bits (0 or 1).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In a Counting filter, we keep a &lt;strong&gt;counter&lt;/strong&gt; (a number).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So when you add an item, you just increment the counter at that index (+1). When you want to delete, you just decrement it (-1).&lt;/p&gt;

&lt;p&gt;Sounds perfect, right? Problem solved! &lt;strong&gt;But here is the major catch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To store a counter (even a small number), you need way more space than a single tiny bit. A standard Bloom filter uses 1 bit per spot. A Counting filter might need 4 bits or more just to hold that number.&lt;/p&gt;

&lt;p&gt;So, we effectively quadrupled the space usage. We solved the deletion problem, but we killed the whole purpose of using a Bloom filter: &lt;strong&gt;Space Efficiency&lt;/strong&gt;. If it takes too much RAM, we might as well just use a HashMap, right?&lt;/p&gt;

&lt;p&gt;We need something that allows deletion without blowing up our memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhamko394lblqdxvdfuag.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhamko394lblqdxvdfuag.jpeg" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuckoo Filters: The Solution
&lt;/h2&gt;

&lt;p&gt;So you might be thinking, &lt;em&gt;"yeah, then there might be a solution."&lt;/em&gt; Yes, there is a fantastic solution known as the &lt;strong&gt;Cuckoo Filter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Does the name give you some hint? &lt;strong&gt;Cuckoo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The bird that doesn't build its own nest; it takes over the nests of other birds. But why this?&lt;/p&gt;

&lt;h3&gt;
  
  
  Cuckoo Hashing: The "Kicking" Strategy
&lt;/h3&gt;

&lt;p&gt;Cuckoo filters work on the principle of partial-key cuckoo hashing. Bloom filters store bits (0 or 1). Cuckoo filters store a &lt;strong&gt;Fingerprint&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a fingerprint?&lt;/strong&gt; Instead of just marking a spot as "occupied," we take a small signature of the data (like an 8-bit or 12-bit hash) and store that. This is the game changer.&lt;/p&gt;

&lt;p&gt;Here is how the "Kicking" works (The Cuckoo Logic):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hash 1:&lt;/strong&gt; String A comes. I hash it and try to put its fingerprint in Nest 1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Collision:&lt;/strong&gt; If Nest 1 is full, we don't just give up. The new entry &lt;strong&gt;kicks out&lt;/strong&gt; the old entry!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-homing:&lt;/strong&gt; The old entry (the victim) is now homeless. It has to find a new spot.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Magic Math:&lt;/strong&gt; In production, we use a math trick (XOR operation) to calculate the "alternative nest" using just the current position and the fingerprint. The victim flies to Nest 2.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Loop:&lt;/strong&gt; If Nest 2 is also full? It kicks that one out.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s a chain reaction. It can very easily turn into an infinite loop. This is the only slight problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt; We assign &lt;strong&gt;Max Iterations&lt;/strong&gt; (let's say 500). It will try to kick items around 500 times. If no solution is reached, then we know the filter is too full, and we need to expand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41v7r11puyz3lmrxliai.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41v7r11puyz3lmrxliai.jpeg" alt=" " width="800" height="1071"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Setup: Buckets and Efficiency
&lt;/h3&gt;

&lt;p&gt;In a real production setup (like in large-scale databases), we don't just have single slots. We use &lt;strong&gt;Buckets&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Imagine the array is divided into buckets, and each bucket has 4 slots.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This means when a hash maps to a bucket, we can fit 4 different items there before we have to start kicking anyone out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This makes the Cuckoo filter insanely dense. We can fill it up to &lt;strong&gt;95% capacity,&lt;/strong&gt; and it still works fast. Bloom filters usually start failing around 50% or 70% full.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why does this solve Deletion?
&lt;/h3&gt;

&lt;p&gt;Because we store Fingerprints, not just bits.&lt;/p&gt;

&lt;p&gt;If I want to delete User A:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I calculate the hash and look in the bucket.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I check the fingerprints stored there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I find the fingerprint that matches User A.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I remove it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since I am matching a specific fingerprint, I am highly unlikely to delete User B by accident. This gives us ** Deletion** without the huge memory cost of Counting Bloom Filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to try it? (RedisBloom)
&lt;/h2&gt;

&lt;p&gt;Now, you might be thinking, &lt;em&gt;"This is cool, but do I have to write all this complex math and XOR logic myself?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;No, my friend. You don't.&lt;/p&gt;

&lt;p&gt;If you are using Redis (and you probably are), there is a module called &lt;strong&gt;RedisBloom&lt;/strong&gt;. It has all this implemented for you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For Bloom Filters:&lt;/strong&gt; You just run &lt;code&gt;BF.ADD key item&lt;/code&gt; and `BF.EXISTS key item. It handles the multiple hashes for you.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For Cuckoo Filters:&lt;/strong&gt; You run &lt;code&gt;CF.ADD key item&lt;/code&gt; the magic command. &lt;code&gt;CF.DEL key item&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s production-ready, highly optimised, and you don't have to worry about the "kicking" logic—it happens behind the scenes. So if you need to optimise space, just load this module, and you are good to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  DIY: Build It Yourself (Pseudocode)
&lt;/h2&gt;

&lt;p&gt;I know some of you are curious. You don't just want to use a library; you want to see the guts of the algorithm.&lt;/p&gt;

&lt;p&gt;If you want to implement this in Python, Go, or Java to learn, here is the blueprint. I’ve written this in pseudocode so you can translate it into your favourite language.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Bloom Filter Logic
&lt;/h3&gt;

&lt;p&gt;This is straightforward. We need a bit array and multiple hash functions.&lt;/p&gt;

&lt;pre&gt;// Configuration
ArraySize = 1000
BitArray = [0] * ArraySize

// Function to Add an Item
Function Add(item):
    idx1 = Hash1(item) % ArraySize
    idx2 = Hash2(item) % ArraySize
    idx3 = Hash3(item) % ArraySize

    BitArray[idx1] = 1
    BitArray[idx2] = 1
    BitArray[idx3] = 1

Function Exists(item):
    idx1 = Hash1(item) % ArraySize
    idx2 = Hash2(item) % ArraySize
    idx3 = Hash3(item) % ArraySize

    IF (BitArray[idx1] == 1 AND BitArray[idx2] == 1 AND BitArray[idx3] == 1):
        RETURN True
    ELSE:
        RETURN False
&lt;/pre&gt;

&lt;h3&gt;
  
  
  2. The Cuckoo Filter Logic
&lt;/h3&gt;

&lt;p&gt;This is where the magic happens. The key here is the "Kicking" loop and the XOR math to find the alternate index.&lt;/p&gt;

&lt;pre&gt;// Configuration
MaxKicks = 500
Buckets = [Size 1000] // Each bucket can hold multiple fingerprints

// Helper: Calculate Fingerprint (a small hash)
Function GetFingerprint(item):
    RETURN Hash(item) -&amp;gt; truncated to 8 bits

// Helper: Calculate Two Possible Locations
Function GetLocations(item, fingerprint):
    idx1 = Hash(item)
    
    // The Magic: We can find idx2 using only idx1 and the fingerprint!
    idx2 = idx1 XOR Hash(fingerprint)
    
    RETURN idx1, idx2

// Function to Add Item
Function Insert(item):
    fp = GetFingerprint(item)
    idx1, idx2 = GetLocations(item, fp)

    // 1. Try to fit in the first nest
    IF Buckets[idx1] has space:
        Buckets[idx1].add(fp)
        RETURN Success

    // 2. Try to fit in the second nest
    IF Buckets[idx2] has space:
        Buckets[idx2].add(fp)
        RETURN Success

    // 3. Both full? Time to KICK!
    current_idx = RandomlyPick(idx1, idx2)
    
    LOOP for count from 0 to MaxKicks:
        // Swap the new fingerprint with the one currently in the bucket
        victim_fp = Buckets[current_idx].swap_with(fp)
        
        // Make the victim the new item to insert
        fp = victim_fp
        
        // Calculate where the victim should go (The Magic XOR again)
        current_idx = current_idx XOR Hash(fp)

        IF Buckets[current_idx] has space:
            Buckets[current_idx].add(fp)
            RETURN Success

    RETURN Failure // Filter is too full, we need to expand!
&lt;/pre&gt;

&lt;p&gt;Try implementing this! It’s one thing to read about "kicking" items, but it’s a whole different feeling when you write that loop and see your code shuffling data around to make space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy Exploration!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
