How Instagram, WhatsApp, Uber & Netflix Would Be Built Today Using Expo Router
When you are building a small hobby project in React Native, you can throw your files almost anywhere and things will work just fine. But when you scale up to apps like Instagram, WhatsApp, or Uber, a messy codebase becomes an absolute nightmare.
At that scale, multiple teams are pushing code simultaneously, features are deeply interconnected, and a single performance bottleneck can ruin the experience for millions of users.
Let's dive under the hood and look at how modern large-scale mobile apps are structured, how to design an architectural blueprint using Expo Router, and the major tradeoffs teams face at production scale.
ποΈ Why Architecture Matters in React Native
In massive applications, architecture isn't just about keeping files tidy. It directly impacts your business and developer experience. A poor architectural setup leads to three major problems:
Spaghetti Code: Tight coupling where changing a line of code in the chat module unexpectedly breaks the checkout screen.
Merge Conflicts: If fifty developers are constantly modifying a single global routing configuration file, team velocity drops to zero.
Performance Degradation: Giant bundle sizes and unoptimized startup times mean users delete the app before it even loads.
To solve this, modern architecture relies heavily on Feature-Based Separation.
Instead of grouping files by their technical type (e.g., keeping all components in one massive folder and all hooks in another), we group files by what they do.
The Feature-Based Blueprint
Inside a feature module, you keep everything required for that specific feature to function independently:
src/
ββ features/
β βββ auth/
β βββ chat/
β β βββ components/ # Chat-specific components
β β βββ hooks/ # Chat business logic
β β βββ services/ # API & WebSocket handlers
β β βββ types.ts # Type definitions
β βββ feed/
β βββ tracking/
βββ shared/ # Global reusable design system & utilities
β βββ components/ # Button, Input, Modal
β βββ network/ # Global Axios/Fetch client
This ensures that if a developer needs to rewrite the entire chat UI, they only ever touch files inside the features/chat/ directory, leaving the rest of the app completely isolated and safe.
π File-System Routing with Expo Router
Expo Router brings file-system routing (similar to Next.js on the web) to React Native. This completely eliminates the need for a massive, centralized NavigationContainer configuration file that everyone has to edit.
Instead, your file tree is your app structure. Expo Router leverages Shared Layouts and Nested Routing seamlessly using native navigation constructs under the hood.
Here is how a real-world, scalable directory layout looks inside the app/ folder:
app/
βββ (auth)/ # Authentication Group
β βββ _layout.tsx # Defines native Stack or empty layout for Auth
β βββ login.tsx # /login
β βββ register.tsx # /register
βββ (main)/ # Main App Protected Group
β βββ _layout.tsx # Defines global layout (e.g., bottom Tabs)
β βββ (tabs)/ # Nested Tab Navigation
β β βββ feed/
β β β βββ _layout.tsx # Stack layout for drilling deep into posts
β β β βββ index.tsx # /feed (Main Feed)
β β β βββ [postId].tsx # /feed/123 (Dynamic Post Detail)
β β βββ chat/
β β β βββ index.tsx # /chat (Conversations List)
β β β βββ [roomId].tsx # /chat/room_abc (Direct Message Screen)
β β βββ profile.tsx # /profile
βββ _layout.tsx # Root Entry: Context Providers & Auth Gate
βββ index.tsx # Initial router redirect logic
The Power of _layout.tsx and Slots
The root _layout.tsx acts as the grand orchestrator. It wraps your app in all necessary global context providers (State, Theme, API Clients) and sets up the foundational navigation shell.
// app/_layout.tsx
import { Slot, useRouter, useSegments } from 'expo-router';
import { useEffect } from 'react';
import { useAuthStore } from '@/features/auth/store';
export default function RootLayout() {
const { isAuthenticated, isInitialized } = useAuthStore();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (!isInitialized) return;
// Check if the user is currently inside the (auth) group folders
const inAuthGroup = segments[0] === '(auth)';
if (!isAuthenticated && !inAuthGroup) {
// Redirect unauthenticated users to login immediately
router.replace('/login');
} else if (isAuthenticated && inAuthGroup) {
// Redirect logged-in users away from auth screens to the main feed
router.replace('/feed');
}
}, [isAuthenticated, isInitialized, segments]);
// Renders the matched child route dynamically
return <Slot />;
}
Why this scales: Adding a new screen is as simple as creating a file. No global navigation config files to fight over, and the folder layout implicitly handles the visual layering of stack and tab bars.
π Networking, State, and Offline-First Caching
Large apps treat data in two distinct ways: server-driven state (API data) and client-driven state (UI toggles, themes, user sessions). Mixing them into a single giant global state store is a recipe for terrible performance.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Root App Layer β
βββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β
βββββββββββββββββ΄ββββββββββββββββ
βΌ βΌ
βββββββββββββββββββββββββ βββββββββββββββββββββββββ
β Server State Layer β β Client State Layer β
β (TanStack Query) β β (Zustand / Jotai) β
βββββββββββββββββββββββββ€ βββββββββββββββββββββββββ€
β β’ Cache Management β β β’ Authentication Tok β
β β’ Optimistic Updates β β β’ App Theme Setting β
β β’ Automatic Retries β β β’ Navigation State β
βββββββββββββββββββββββββ βββββββββββββββββββββββββ
1. Server State (The API Layer)
For handling APIs, large systems rely heavily on TanStack Query (React Query) rather than raw useEffect calls or global state dispatch patterns. It manages caching, deduplicates requests, and handles background synchronization out of the box.
2. Client State
For pure local client state, lightweight libraries like Zustand or Jotai are preferred over classic Redux. They use a decoupled selector model, preventing your entire app layout from re-rendering when a single local variable changes.
3. Offline-First Caching (The Netflix/Instagram Model)
When a user opens Instagram on a subway, they don't see a blank loading spinner; they see the feed items cached from their last session.
To achieve this, we hook up TanStack Query to an ultra-fast, local key-value database like MMKV instead of the sluggish built-in AsyncStorage.
// shared/network/queryClient.ts
import { QueryClient } from '@tanstack/react-query';
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // Keep unused cache alive for 24 hours
staleTime: 1000 * 60 * 5, // Consider data fresh for 5 minutes
},
},
});
// Sync data seamlessly to ultra-fast native memory storage
export const clientPersister = {
persistClient: async (client: any) => {
storage.set('app-cache', JSON.stringify(client));
},
restoreClient: async () => {
const cache = storage.getString('app-cache');
return cache ? JSON.parse(cache) : undefined;
},
removeClient: async () => {
storage.delete('app-cache');
},
};
β‘ Real-Time Subsystems at Scale
High-performance real-time features require highly optimized architecture to keep your JS thread running at a smooth 60 frames per second.
π¬ Chat Systems (The WhatsApp Model)
For chat systems, maintaining long-lived persistent connections is essential. While simple apps use raw WebSockets or Socket.io, massive chat applications leverage binary protocols like MQTT or structured buffers like Protocol Buffers (Protobuf) over TCP.
This minimizes the payload size down to the absolute bare minimum bytes, significantly saving device battery life and mobile network data usage.
π Live Ride Tracking (The Uber Model)
If you update a map marker every single time a driver's GPS emits a tiny change, your app will choke and freeze.
To make tracking fluid, engineers implement two core strategies:
Throttling & Interpolation: We restrict incoming coordinates to tick once every few seconds, and then use mathematical animations (like layout transitions or custom re-animating drivers) to smoothly slide the vehicle marker across the map layout.
Geohashing: The backend partitions map spaces into precise grid cells, meaning the client only listens to coordinates within their immediate spatial grid cluster.
π Production Performance & Startup Optimization
No matter how great your features are, users will abandon an app if it takes more than two seconds to interactive on startup.
1. Bundle Splitting via Lazy Evaluation
By default, JavaScript evaluates everything loaded at boot time. If your chat code is massive, it slows down the feed load time. To combat this, look into utilizing dynamic imports or chunked modules, ensuring you only initialize deep code blocks right when they are needed.
2. Native Machinery: FlashList vs FlatList
When rendering thousands of heavy feed posts, standard FlatList recreates DOM-like components aggressively, causing garbage collection spikes and UI stutter. Large teams swap to Shopifyβs FlashList, which recycles platform native views under the hood without continuously destroying and recreating UI nodes.
3. The Ultimate Mobile Engine: Hermes
Always ensure Hermes is enabled in your project configurations. Hermes is a JavaScript engine custom-built by Meta for running React Native efficiently.
Instead of parsing raw JS source code at runtime on the user's phone, Hermes compiles your JavaScript bundle into optimized bytecode ahead-of-time (AOT) during your production build process. This slashes app startup times down drastically and significantly lowers overall memory consumption.
βοΈ Architectural Tradeoffs: The Realities of Scale
Every architectural benefit has a hidden cost. When making decisions, you have to choose which pain point your team is willing to manage:
| Choice | The Pros | The Cons | Who Does It? |
|---|---|---|---|
| Monolith (All Features in One Repo) | Easy to share types, simpler CI/CD pipelines, single dependency management. | Slow CI/CD pipelines, messy git histories, risky releases. | Early-mid stage scale apps. |
| Micro-Frontends / Monorepos | High team isolation, modular deployments, independent versioning control. | Massive overhead setup, runtime version conflicts, highly complex build systems. | Uber / Netflix |
| Expo Continuous Native Workflow | Rapid OTA updates, unified JavaScript tooling ecosystem, zero native configuration dread. | Heavy dependency on third-party plugin configurations for custom native libraries. | Modern enterprise fast-moving scale apps. |
| Pure Custom Native Bridges | Maximum performance optimization, fine-grained access to bare metal device APIs. | High maintenance overhead, requires dedicated Swift/Kotlin expert engineering teams. | Meta (Instagram / WhatsApp) |
There is no "perfect" stack. The ideal choice is always the minimal structure that enables your engineering teams to ship code independently, safely, and predictably without tripping over one another.