# How Instagram, WhatsApp, Uber & Netflix Would Be Built Today Using Expo Router



How Instagram, WhatsApp, Uber & Netflix Would Be Built Today Using Expo Router

> Architecture isn't about the code you write. It's about the decisions you make before writing any code.

If you've ever opened a React Native project and stared at a `components/` folder with 200 files in it, this article is for you. Today, we're going to think like the engineers who built Instagram, WhatsApp, Uber, and Netflix — and explore how you'd structure those apps if you were starting from scratch today, using Expo Router.

This isn't a UI clone tutorial. We're not building Instagram's Reels UI or Uber's map screen pixel-by-pixel. We're thinking about **architecture** — how to organize code, navigate between screens, handle state, sync data in real time, and scale without losing your mind.

Let's go.

* * *

## 1\. Why Simple Folder Structures Fail at Scale

### The "It Works on My Machine" Phase

Every React Native project starts simple. You have a `screens/` folder, a `components/` folder, maybe a `utils/` folder, and a single `App.tsx`. It's clean, understandable, and everyone on the team (usually just you) knows where everything lives.

Then the app grows.

You add authentication, then a profile screen, then a feed, then notifications, then settings, then DMs. Suddenly your `screens/` folder has 40 files, your `components/` folder has 150+ components with names like `FeedCard.tsx`, `FeedCardNew.tsx`, and `FeedCardV2Final.tsx`. Your `App.tsx` has 600 lines of nested navigators. New team members spend two days just understanding the project before writing a single line of code.

This is not a theoretical problem. This is how most apps die.

### What Goes Wrong

The root causes of a failing folder structure are almost always the same:

*   **Feature logic is split across the codebase.** The authentication screen, its API call, its state slice, its validation logic, and its UI components all live in different folders. To understand auth, you jump between 6 different directories.
    
*   **No clear ownership.** When something breaks, no one knows which folder "owns" the broken piece.
    
*   **Shared components become a graveyard.** A `components/` folder that's shared by everyone is owned by no one. It accumulates dead code that nobody dares delete.
    
*   **Routing becomes a monolith.** A single navigation file managing 40+ screens becomes unmaintainable.
    

### The Mental Shift: From Files to Features

The solution isn't a better folder name. It's a different way of thinking. Instead of organizing code by **type** (all screens together, all components together), you organize it by **feature** (everything related to messaging lives in the `messaging/` module).

This is the shift from small-app thinking to production engineering thinking. And Expo Router makes this shift natural.

* * *

## 2\. Why Architecture Matters in React Native Applications

### Mobile Has Unique Constraints

Web apps can get away with messy architecture longer because the browser handles a lot — routing, caching, back navigation. In React Native, you own all of it.

*   **Navigation is your responsibility.** There's no browser URL bar. Every screen transition, deep link, and back press is code you write and maintain.
    
*   **Performance is visible.** A janky scroll, a slow startup, a flickering list — users feel it immediately. Architecture choices directly affect frame rate.
    
*   **Offline behavior is expected.** Mobile users expect apps to work in tunnels, on planes, and with bad connectivity. This requires deliberate caching and sync architecture.
    
*   **Bundle size matters.** A 50MB app has lower install rates than a 10MB app. Architecture decisions affect how code is split and loaded.
    

### The Cost of Rearchitecting Later

Bad architecture in mobile is especially painful to fix because:

1.  Deep links are tied to your navigation structure. Changing routes breaks links shared by users.
    
2.  State management changes require rewriting screens, not just services.
    
3.  API layer changes ripple across every screen that touches data.
    

The engineers at Instagram, Uber, and Netflix didn't get their architecture right because they were smarter. They got it right because they invested in it *early*, before scale made changes expensive.

* * *

## 3\. Folder Architecture Using Expo Router

### How Expo Router Changes Everything

Expo Router brings file-based routing to React Native — the same mental model as Next.js. Your file structure **is** your navigation structure. This is a massive architectural win because:

*   Routes are self-documenting. You can read the folder structure and understand the app's navigation.
    
*   Deep linking works automatically. Every route is a URL.
    
*   Shared layouts are trivial. A `_layout.tsx` wraps all screens in its directory.
    
*   Code splitting is natural. Features map to folders, folders map to routes.
    

### Production-Grade Expo Router Folder Structure

Here's how a production app at scale would be structured:

```plaintext
my-app/
├── app/                          # Expo Router root — file-based routes
│   ├── _layout.tsx               # Root layout (fonts, providers, auth guard)
│   ├── index.tsx                 # Entry redirect (to onboarding or home)
│   │
│   ├── (auth)/                   # Auth group — no tab bar
│   │   ├── _layout.tsx
│   │   ├── login.tsx
│   │   ├── register.tsx
│   │   └── forgot-password.tsx
│   │
│   ├── (tabs)/                   # Main app tab layout
│   │   ├── _layout.tsx           # Tab bar configuration
│   │   ├── index.tsx             # Home / Feed
│   │   ├── explore.tsx
│   │   ├── notifications.tsx
│   │   └── profile.tsx
│   │
│   ├── (modals)/                 # Full-screen modals
│   │   ├── _layout.tsx
│   │   ├── create-post.tsx
│   │   └── story-viewer.tsx
│   │
│   └── +not-found.tsx            # 404 screen
│
├── src/
│   ├── features/                 # Feature-based modules
│   │   ├── auth/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   ├── store/
│   │   │   └── types.ts
│   │   ├── feed/
│   │   ├── messaging/
│   │   ├── profile/
│   │   ├── notifications/
│   │   └── player/               # e.g., for Netflix-style video
│   │
│   ├── shared/                   # Truly shared, stable code
│   │   ├── components/           # Button, Avatar, Card, etc.
│   │   ├── hooks/                # useDebounce, useThrottle, etc.
│   │   ├── utils/
│   │   └── constants/
│   │
│   ├── lib/                      # Third-party integrations
│   │   ├── api/                  # Axios/Fetch instance + interceptors
│   │   ├── socket/               # WebSocket/Socket.io client
│   │   ├── storage/              # MMKV / AsyncStorage wrapper
│   │   └── analytics/
│   │
│   └── store/                    # Global state (Zustand / Redux)
│       ├── auth.store.ts
│       ├── app.store.ts
│       └── index.ts
│
├── assets/
├── constants/
└── app.json
```

### Understanding Route Groups

Expo Router's **route groups** (folders wrapped in parentheses) are the architectural backbone of a production app. They:

*   Group screens that share a layout without affecting the URL/path.
    
*   Allow you to have tab-bar screens and auth screens as completely separate layout trees.
    
*   Enable modals, drawers, and stacks to coexist cleanly.
    

```plaintext
(auth)/     → No tab bar, simple stack
(tabs)/     → Tab bar always visible
(modals)/   → Slides over current screen
```

This maps directly to how Instagram works: the feed, explore, and profile screens share a tab bar. The story viewer and DM composer are modals. The login screen has none of it.

* * *

## 4\. Feature-Based Separation in Large Applications

### One Module, One Responsibility

Feature-based architecture means each feature directory contains everything it needs:

```plaintext
src/features/messaging/
├── components/
│   ├── MessageBubble.tsx
│   ├── ChatHeader.tsx
│   └── TypingIndicator.tsx
├── hooks/
│   ├── useMessages.ts
│   ├── useSocket.ts
│   └── useTypingStatus.ts
├── services/
│   ├── messaging.api.ts         # API calls
│   └── messaging.socket.ts      # Socket event handlers
├── store/
│   └── messaging.store.ts       # Zustand/Redux slice
└── types.ts
```

When a new engineer joins and needs to work on chat, they go to `src/features/messaging/`. That's it. Everything they need is there.

### Rules for Feature Modules

1.  **Features can use** `shared/`, but shared cannot import from features.
    
2.  **Features should not directly import from other features.** Cross-feature communication goes through the global store or events.
    
3.  **API calls live in the feature's** `services/` **folder**, not in a global API folder.
    
4.  **If a component is used in more than two features**, it graduates to `shared/components/`.
    

### WhatsApp Example: Feature Boundaries

In a WhatsApp-style app, the feature breakdown would look like:

| Feature | Owns |
| --- | --- |
| `messaging/` | Chats list, chat room, message bubbles, socket events |
| `contacts/` | Contact list, add contact, contact search |
| `status/` | Status feed, story-style updates |
| `calls/` | Audio/video call UI, WebRTC logic |
| `profile/` | User profile, settings |
| `auth/` | Login, OTP verification, session management |

Each of these is a self-contained unit. The `messaging/` feature talks to the `contacts/` feature only through shared store or passed props — never through a direct import.

* * *

## 5\. Navigation Architecture for Scalable Apps

### The Three Navigation Primitives

Every mobile app, no matter how complex, is built on three navigation primitives:

1.  **Stack** — Push/pop screens. Used for drilling into detail views.
    
2.  **Tabs** — Side-by-side top-level sections. The bottom tab bar.
    
3.  **Modal** — Overlays the current context. Full-screen modals, bottom sheets.
    

Expo Router composes these naturally through nested layouts.

### Navigation Hierarchy for Large Applications

```plaintext
Root Layout (_layout.tsx)
├── Auth Stack  ← if not authenticated
│   ├── Login
│   ├── Register
│   └── Forgot Password
│
└── App (Tabs)  ← if authenticated
    ├── Tab: Feed
    │   └── Stack
    │       ├── Feed Home
    │       ├── Post Detail
    │       └── Comment Thread
    ├── Tab: Explore
    │   └── Stack
    │       ├── Explore Home
    │       └── Search Results
    ├── Tab: Messages
    │   └── Stack
    │       ├── Conversations List
    │       └── Chat Room
    └── Tab: Profile
        └── Stack
            ├── Profile Home
            └── Edit Profile

Modals (rendered above tabs)
├── Camera / Create Post
├── Story Viewer
└── Share Sheet
```

In Expo Router, this becomes:

```tsx
// app/(tabs)/_layout.tsx
export default function TabLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="index" options={{ title: "Home" }} />
      <Tabs.Screen name="explore" options={{ title: "Explore" }} />
      <Tabs.Screen name="messages" options={{ title: "Messages" }} />
      <Tabs.Screen name="profile" options={{ title: "Profile" }} />
    </Tabs>
  );
}
```

### Instagram-Style Navigation

Instagram's navigation is a master class in this pattern:

*   Main tabs: Home, Search, Reels, Shop, Profile.
    
*   Tapping a post from the feed pushes a detail screen **within the tab's stack**.
    
*   Stories open as a **modal** — they slide up over the feed without changing your tab context.
    
*   The DM icon in the top-right opens another stack that sits **outside the tab layout**.
    

With Expo Router, the stories and DMs are modals. The feed detail is a stack push. No special navigation library configuration needed — the folder structure defines it all.

* * *

## 6\. Authentication Flow Architecture

### The Protected Route Pattern

The most critical navigation decision in any app is: how do you guard routes from unauthenticated users?

With Expo Router, the clean pattern is to handle this in the **root layout**:

```tsx
// app/_layout.tsx
export default function RootLayout() {
  const { isAuthenticated, isLoading } = useAuthStore();

  if (isLoading) return <SplashScreen />;

  return (
    <Stack>
      {isAuthenticated ? (
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      ) : (
        <Stack.Screen name="(auth)" options={{ headerShown: false }} />
      )}
    </Stack>
  );
}
```

### Authentication Flow with Protected Routes

```plaintext
App Launch
    │
    ▼
Check token in SecureStore / MMKV
    │
    ├── Token exists → Validate with server
    │       ├── Valid   → Navigate to (tabs)/
    │       └── Expired → Refresh token
    │               ├── Success → Navigate to (tabs)/
    │               └── Fail    → Navigate to (auth)/login
    │
    └── No token → Navigate to (auth)/login

Login Screen
    │
    ▼
POST /auth/login
    │
    ├── Success → Store access + refresh token → Navigate to (tabs)/
    └── Failure → Show error, stay on (auth)/login
```

### Token Management Layer

Never scatter auth logic across screens. Centralize it:

```plaintext
src/lib/auth/
├── tokenStorage.ts     # Secure read/write of access & refresh tokens
├── tokenRefresh.ts     # Interceptor logic for expired tokens
└── authGuard.ts        # Route protection hook
```

The API client (Axios/Fetch) uses an interceptor that automatically attaches the access token to every request, and automatically refreshes it on 401 responses — **without any screen knowing this is happening.**

### Biometric Authentication (Instagram/WhatsApp Pattern)

Large apps also implement app-lock via biometrics:

```plaintext
App returns to foreground
    │
    ▼
Check if biometric lock is enabled
    │
    ├── Enabled → Show biometric prompt (Face ID / Fingerprint)
    │       ├── Success → Unlock app
    │       └── Fail    → Show PIN fallback
    │
    └── Disabled → Resume directly
```

* * *

## 7\. State Management Strategies for Large Apps

### Don't Use One Solution for Everything

The most common mistake is reaching for Redux for everything. Production apps use **multiple state management layers** based on what kind of state it is:

| State Type | Solution | Example |
| --- | --- | --- |
| Server data (fetched) | React Query / TanStack Query | Feed posts, user profiles |
| Global UI state | Zustand | Theme, modal visibility, auth status |
| Real-time data | Zustand + WebSocket | Chat messages, live locations |
| Form state | React Hook Form | Login, post creation |
| URL/navigation state | Expo Router | Current screen, params |
| Persistent state | MMKV / AsyncStorage | Auth tokens, preferences |

### API Layer + State Management Flow

```plaintext
User Action (e.g., pulls to refresh feed)
    │
    ▼
React Query: useFeedQuery()
    │
    ├── Cache hit → Return stale data immediately, refetch in background
    │
    └── Cache miss / stale
            │
            ▼
        API Service Layer (src/lib/api/)
            │
            ├── Attach auth token (interceptor)
            ├── POST /feed?cursor=...
            └── Response
                    │
                    ├── Success → Update React Query cache → UI re-renders
                    └── Error   → Retry logic → Error boundary
```

### Zustand for Global UI State

Zustand is the sweet spot for global UI and auth state — lightweight, no boilerplate, works perfectly with Expo Router:

```ts
// src/store/auth.store.ts
interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  setUser: (user: User) => void;
  clearAuth: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  isAuthenticated: false,
  setUser: (user) => set({ user, isAuthenticated: true }),
  clearAuth: () => set({ user: null, isAuthenticated: false }),
}));
```

### Netflix Pattern: Hybrid State

Netflix uses a layered state strategy:

*   **TanStack Query** for content catalog (search results, category rows, show details — all server state with aggressive caching).
    
*   **Zustand** for playback state (current episode, playback position, quality setting).
    
*   **MMKV** for persisted watch progress (synced to server periodically, not on every second).
    
*   **Context API** for player UI state (overlay visibility, control bar) — scoped and doesn't need to be global.
    

* * *

## 8\. API Handling and Networking Layers

### The API Client Architecture

Never call `fetch()` directly in a component. Build a proper API layer:

```plaintext
src/lib/api/
├── client.ts          # Axios instance with base URL, timeout
├── interceptors.ts    # Auth token attach, refresh on 401, error logging
├── endpoints.ts       # Centralized endpoint constants
└── types.ts           # API response types
```

```ts
// src/lib/api/client.ts
const apiClient = axios.create({
  baseURL: process.env.EXPO_PUBLIC_API_URL,
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' },
});

// Attach token on every request
apiClient.interceptors.request.use(async (config) => {
  const token = await tokenStorage.getAccessToken();
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Handle 401 — refresh token silently
apiClient.interceptors.response.use(
  (res) => res,
  async (error) => {
    if (error.response?.status === 401) {
      await refreshAccessToken();
      return apiClient(error.config); // retry
    }
    return Promise.reject(error);
  }
);
```

### Feature-Level Service Files

Each feature owns its API calls:

```ts
// src/features/feed/services/feed.api.ts
export const feedApi = {
  getFeed: (cursor?: string) =>
    apiClient.get<FeedResponse>('/feed', { params: { cursor } }),
  likePost: (postId: string) =>
    apiClient.post(`/posts/${postId}/like`),
  getComments: (postId: string) =>
    apiClient.get<CommentsResponse>(`/posts/${postId}/comments`),
};
```

### Uber's Location Data Strategy

Uber doesn't hit a REST API for live location updates. They use a combination:

*   **REST API** for ride requests, pricing, driver info — standard request/response.
    
*   **WebSocket** for live driver location updates — continuous stream.
    
*   **HTTP polling** as fallback when WebSocket disconnects.
    
*   **Client-side prediction** — the map moves the pin smoothly between received coordinates using interpolation, so it doesn't jump every second.
    

* * *

## 9\. Real-Time Systems

### Chat Systems (WhatsApp Architecture)

WhatsApp's real-time chat is built on reliable message delivery with guaranteed order. In a React Native app, this translates to:

```plaintext
src/features/messaging/
├── services/
│   └── messaging.socket.ts     # All socket event logic
└── store/
    └── messaging.store.ts      # Local message state
```

**The messaging socket layer:**

```ts
// Simplified socket event flow
socket.on('message:receive', (message: Message) => {
  // 1. Optimistically add to local store
  messagingStore.addMessage(message);
  // 2. Persist to local DB (WatermelonDB / SQLite)
  localDb.messages.insert(message);
  // 3. Send delivery receipt
  socket.emit('message:delivered', { messageId: message.id });
});
```

**Realtime Messaging Architecture Flow:**

```plaintext
User types and sends message
        │
        ▼
Optimistic update → Add to local state immediately (show in UI)
        │
        ▼
Emit via WebSocket: socket.emit('message:send', payload)
        │
        ├── Server ACK received
        │       └── Update message status: sent → delivered
        │
        └── ACK timeout (5s)
                └── Queue for retry → Re-emit on reconnect
```

### Live Updates (Instagram Feeds)

Instagram's feed doesn't use WebSockets for new posts — that would be too expensive at scale. Instead, they use:

*   **Long polling** or **Server-Sent Events (SSE)** for like counts and comment counts.
    
*   **Background refresh** every 60 seconds when the app is in focus.
    
*   **Push notifications** for activity (new follower, comment) — which deep link into Expo Router routes.
    

```ts
// Using TanStack Query for Instagram-style live feed data
const { data, refetch } = useQuery({
  queryKey: ['feed'],
  queryFn: feedApi.getFeed,
  refetchInterval: 60_000, // refetch every 60s
  staleTime: 30_000,       // treat data as fresh for 30s
});
```

### Ride Tracking (Uber Architecture)

Uber's live tracking is one of the most technically demanding real-time features in any mobile app:

```plaintext
Driver's phone
    │ emits location every 4s via WebSocket
    ▼
Location service (backend)
    │ broadcasts to rider's WebSocket channel
    ▼
Rider's React Native app
    │
    ├── Receives coordinate {lat, lng, bearing}
    ├── Stores in Zustand: rideStore.updateDriverLocation()
    ├── Animates marker on map (interpolation over 4s)
    └── Updates ETA label
```

**In React Native with Expo Router:**

```ts
// src/features/ride/hooks/useDriverLocation.ts
export function useDriverLocation(rideId: string) {
  const updateDriverLocation = useRideStore((s) => s.updateDriverLocation);

  useEffect(() => {
    const channel = supabase
      .channel(`ride:${rideId}`)
      .on('broadcast', { event: 'location' }, ({ payload }) => {
        updateDriverLocation(payload);
      })
      .subscribe();

    return () => supabase.removeChannel(channel);
  }, [rideId]);
}
```

* * *

## 10\. Offline-First Support and Caching

### Why Offline Matters More on Mobile

A web user with no internet just sees an error. A mobile user with no internet expects the app to work — or at least show them their last known state. Instagram still shows your feed. WhatsApp still shows your conversations. Spotify still plays downloaded music.

Offline-first architecture means: **assume the network is unreliable and design around it.**

### The Offline Cache Synchronization Flow

```plaintext
App opens / user navigates to screen
        │
        ▼
Check local cache (MMKV / WatermelonDB / SQLite)
        │
        ├── Cache hit → Render immediately from cache
        │       │
        │       └── If online → Fetch fresh data in background
        │               └── On response → Diff and update cache → Re-render if changed
        │
        └── Cache miss + offline → Show empty state with "No connection" UI
                       + offline → Queue actions (likes, messages) for retry on reconnect
```

### Tools for Offline-First in React Native

| Layer | Tool | Use Case |
| --- | --- | --- |
| Key-value cache | MMKV | User preferences, auth tokens, last-seen timestamps |
| Structured local DB | WatermelonDB | Chat messages, draft posts, feed cache |
| Query cache | TanStack Query | API response caching with stale-while-revalidate |
| Optimistic updates | TanStack Query | Likes, follows — update UI before server confirms |
| Background sync | Expo Background Fetch | Sync queued actions when connectivity returns |

### WhatsApp's Offline Message Queue

```ts
// When offline, queue outgoing messages
async function sendMessage(chatId: string, content: string) {
  const message = createOptimisticMessage(content); // status: 'queued'
  messagingStore.addMessage(message);               // show in UI

  if (!networkStore.isOnline) {
    await messageQueue.enqueue({ chatId, message }); // persist to local DB
    return;
  }

  try {
    await messagingApi.sendMessage(chatId, content);
    messagingStore.updateStatus(message.id, 'sent');
  } catch {
    messagingStore.updateStatus(message.id, 'failed');
  }
}

// Drain the queue on reconnect
networkStore.onReconnect(async () => {
  const queued = await messageQueue.getAll();
  for (const item of queued) {
    await sendMessage(item.chatId, item.message.content);
    await messageQueue.remove(item.id);
  }
});
```

* * *

## 11\. App Startup Optimization Techniques

### The Problem: Slow Cold Start

Cold start time — the time from tapping the app icon to seeing your first interactive screen — directly impacts retention. Every second of loading loses users. Facebook's internal studies have shown that every 100ms of startup latency reduces engagement meaningfully.

### App Startup Lifecycle Optimization

```plaintext
App process starts
    │
    ▼
[Phase 1: Critical bootstrap — must be fast]
    ├── Load fonts (preload in _layout.tsx with expo-font)
    ├── Read auth token from MMKV (synchronous, no async)
    └── Determine initial route (auth or tabs)
    │
    ▼
[Phase 2: Render first screen — show something immediately]
    ├── Show splash screen or skeleton
    └── Start background tasks (non-blocking):
            ├── Validate token with server
            ├── Prefetch critical data (e.g., first page of feed)
            └── Initialize analytics, crash reporting
    │
    ▼
[Phase 3: Hydration — fill in the real content]
    ├── Replace skeletons with real data
    ├── Initialize WebSocket connection
    └── Schedule background sync
```

### Key Optimization Techniques

**1\. Use MMKV instead of AsyncStorage for token reads** MMKV is synchronous and 10x faster than AsyncStorage. Reading the auth token on startup becomes instantaneous.

**2\. Defer non-critical initialization** Don't initialize analytics, Sentry, or push notifications in the root layout. Initialize them *after* the first screen renders.

**3\. Prefetch above-the-fold data** While showing the splash/skeleton, start fetching the feed. By the time the user sees the feed, data is already loading.

**4\. Use** `expo-splash-screen` **to control dismissal** Don't dismiss the splash screen until fonts are loaded and the auth check is complete. This prevents a flash of unstyled content.

```tsx
// app/_layout.tsx
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const [fontsLoaded] = useFonts({ ... });
  const { isAuthChecked } = useAuthStore();

  useEffect(() => {
    if (fontsLoaded && isAuthChecked) {
      SplashScreen.hideAsync();
    }
  }, [fontsLoaded, isAuthChecked]);
}
```

**5\. Lazy-load heavy screens** Netflix's video player, Uber's map, Instagram's camera — these shouldn't be loaded at startup. Use React's `lazy()` and `Suspense` to load them on demand.

* * *

## 12\. Performance Considerations in Production Apps

### The Performance Killers

Most React Native performance issues come from the same handful of mistakes:

| Problem | Impact | Fix |
| --- | --- | --- |
| Re-rendering entire lists | Jank while scrolling | Use `FlashList` instead of `FlatList` |
| Heavy components on main thread | Dropped frames | Move to worklets with `react-native-reanimated` |
| Inline object/function props | Constant re-renders | `useCallback`, `useMemo`, `memo()` |
| Synchronous storage reads | Blocked JS thread | MMKV (sync), avoid AsyncStorage in hot paths |
| Large images not cached | Slow feed scroll | `expo-image` with memory + disk cache |
| Uncontrolled re-renders from global store | UI lag | Use selectors, subscribe to minimal state |

### Instagram's Feed Performance

Instagram's feed scrolls at 60fps with high-res images and videos. The key techniques:

*   `FlashList` (by Shopify) instead of `FlatList` — recycles cell components more efficiently.
    
*   **Progressive image loading** — show a blurred thumbnail, then full image.
    
*   `expo-image` — built-in disk and memory caching, reduces re-downloads.
    
*   **Virtualization** — only the ~5 items visible + ~10 off-screen are rendered.
    
*   **Video pause on scroll** — videos pause when they leave the viewport, freeing GPU resources.
    

### Netflix's Video Playback Architecture

Netflix has the most demanding performance requirements:

*   **Adaptive bitrate streaming (HLS/DASH)** — quality adjusts to network speed automatically.
    
*   **Pre-buffering** — the next episode starts buffering 30s before the current one ends.
    
*   **Background download** — offline downloads happen in the background via `expo-file-system` + `react-native-background-downloader`.
    
*   **Playback state isolation** — the player's Zustand store is separate from the app store. Player state changes don't cause the rest of the app to re-render.
    

* * *

## 13\. Shared Layouts and Nested Routing in Expo Router

### The Power of `_layout.tsx`

Every folder in your `app/` directory can have a `_layout.tsx` that wraps all screens inside it. This is how you share headers, tab bars, sidebars, and context providers without repeating them.

```plaintext
app/
├── _layout.tsx              ← Wraps EVERYTHING (fonts, providers, auth guard)
├── (tabs)/
│   ├── _layout.tsx          ← Wraps all tabs (tab bar itself)
│   └── (feed)/
│       ├── _layout.tsx      ← Wraps feed screens (shared header context)
│       ├── index.tsx        ← /feed
│       └── [postId].tsx     ← /feed/123
```

### Dynamic Routes for Content Apps

Dynamic routes (`[id]`) are essential for content apps:

```plaintext
app/(tabs)/(feed)/
├── index.tsx              → /feed
├── [postId].tsx           → /feed/abc123
└── [postId]/
    └── comments.tsx       → /feed/abc123/comments
```

This means deep links like `myapp://feed/abc123` work automatically. No manual deep link handling. Expo Router resolves it to the right screen.

### Nested Navigation: Instagram DMs Example

Instagram's DM flow sits outside the main tab structure — it's like a second app that slides over:

```plaintext
app/
├── (tabs)/                 ← Main tab navigation
│   ├── index.tsx           ← Feed
│   └── profile.tsx         ← Profile
│
└── (messages)/             ← Separate stack, slides over tabs
    ├── _layout.tsx         ← Stack navigator, no tab bar
    ├── index.tsx           ← Conversations list
    └── [chatId].tsx        ← Individual chat room
```

When a user taps the DM icon, they're pushed into the `(messages)` stack, which renders over the tabs but preserves the tab bar state underneath.

* * *

## 14\. Scalability Challenges and How Each App Solves Them

### Instagram: Feeds and Media at Scale

**Challenge:** A user follows 1,000 accounts. Computing a personalized feed of recent posts from all of them in real time is expensive. Serving full-resolution images to 2 billion users is expensive.

**Architecture decisions:**

*   **Fan-out on write vs. fan-out on read:** Instagram pre-computes feeds for most users (fan-out on write) but computes feeds on-demand (fan-out on read) for users who follow celebrities with 100M+ followers.
    
*   **CDN for media:** Every image goes through a CDN. The app requests the image at the exact pixel dimensions needed, not the original.
    
*   **Progressive JPEG + lazy loading:** Feed items below the fold are not loaded until the user scrolls near them.
    
*   **Expo Router impact:** The feed screen uses a dynamic route for post details (`[postId].tsx`). Stories use a modal layout to avoid breaking feed scroll position.
    

### WhatsApp: Realtime Messaging at Scale

**Challenge:** 100 billion messages per day. Message ordering must be consistent. Messages must arrive even if the receiver is offline for 30 days.

**Architecture decisions:**

*   **XMPP-derived protocol:** Messages are not stored on servers by default — end-to-end encrypted and pushed to device.
    
*   **Message queue for offline delivery:** Messages are held on server until the device comes online, then delivered in order.
    
*   **Local database first:** All messages are written to local SQLite (WatermelonDB in React Native equivalent) first. The server is the source of truth for delivery but the device DB is the source of truth for display.
    
*   **Expo Router impact:** Chat rooms are dynamic routes (`[chatId].tsx`). Typing indicators and online status come from a WebSocket layer, completely separate from the API layer.
    

### Uber: Maps and Live Location at Scale

**Challenge:** Tracking millions of active drivers and riders simultaneously. Matching the nearest available driver in real time. Routing that updates as traffic changes.

**Architecture decisions:**

*   **Geohashing:** The map is divided into grid cells. Uber only tracks drivers in cells near the rider's cell — not all drivers everywhere.
    
*   **Location updates decoupled from map rendering:** The app receives location updates every 4 seconds but animates the map pin continuously using interpolation.
    
*   **Separate service for each phase:** Ride request, driver matching, active ride tracking, and payment are handled by separate backend microservices. The app consumes them through a unified API gateway.
    
*   **Expo Router impact:** The ride tracking screen is a modal that slides over the home map. It subscribes to a Supabase/Socket.io channel scoped to the active `rideId` — which comes from the route param.
    

### Netflix: Heavy Content Delivery at Scale

**Challenge:** Streaming video to 300M subscribers simultaneously. Recommending content that keeps users watching. Handling content libraries with thousands of titles across dozens of markets.

**Architecture decisions:**

*   **Adaptive bitrate streaming:** The player automatically switches between 240p and 4K based on current bandwidth.
    
*   **Open Connect (CDN):** Netflix operates its own CDN appliances placed inside ISPs. Popular content is cached at the ISP level, dramatically reducing streaming latency.
    
*   **Recommendation as a separate layer:** The recommendation engine runs server-side. The app just receives a list of `contentId` arrays for each row ("Continue Watching", "Top Picks for You"). It doesn't know the algorithm.
    
*   **Expo Router impact:** Content is browsed via tabs (Home, Search, Downloads, Profile). The player is a full-screen modal with hidden navigation chrome. The `contentId` comes from a dynamic route (`[contentId].tsx`), making deep links from push notifications trivially simple.
    

* * *

## 15\. Tradeoffs and Architectural Decisions at Scale

### When to Use React Query vs. Zustand vs. Context

| Choose | When |
| --- | --- |
| React Query | Data that comes from a server, needs caching, pagination, or background refresh |
| Zustand | UI state that's global but not server-derived (auth status, theme, modal open/closed) |
| Context API | State scoped to a subtree (player state, form state, a specific feature) |
| Local `useState` | State that's entirely local to a single component |

### Tradeoffs Every Team Faces

**Expo Router vs. React Navigation directly**

Expo Router adds file-based conventions but requires your project to follow its folder structure. React Navigation gives more control at the cost of more boilerplate. For new projects, Expo Router wins. For migrating existing projects, the cost is higher.

**TanStack Query vs. RTK Query**

Both are excellent. TanStack Query has a smaller API surface and is framework-agnostic. RTK Query integrates tightly with Redux. If your team already uses Redux, RTK Query is less friction. If starting fresh, TanStack Query is simpler.

**WatermelonDB vs. SQLite directly vs. no local DB**

WatermelonDB is the right choice for apps with complex relational data (messaging, social feeds). SQLite directly is lower-level but works. Most apps don't need a local DB — MMKV + TanStack Query cache covers 80% of use cases.

**Monorepo vs. separate repos**

At Uber and Meta scale, different teams own different parts of the app. A monorepo (Nx or Turborepo) allows shared code while keeping team ownership clear. For solo developers and small teams, a single repo is fine.

### The Most Important Decision: Invest in Architecture Early

Here's the honest truth: you will not build Instagram, WhatsApp, Uber, or Netflix. But you will build something that grows beyond what you planned for. The folder structure you choose in week one will still be the folder structure you're arguing about in year three.

Feature-based architecture with Expo Router's file-based routing gives you a system that scales with the team and the product. Not because it's perfect, but because its conventions force clarity — every engineer knows where to look, every new feature knows where to live.

That clarity, at scale, is everything.

* * *

## Summary: Key Architectural Principles

| Principle | Why It Matters |
| --- | --- |
| File-based routing with Expo Router | Routes are self-documenting; deep links are free |
| Feature-based folder structure | Clear ownership; no spaghetti imports |
| Separate state layers (Query + Zustand + MMKV) | Right tool for right state type |
| Centralized API client with interceptors | Auth and error handling in one place |
| Offline-first with optimistic updates | Resilient UX on unreliable networks |
| Lazy-load heavy features | Fast startup; load on demand |
| Real-time via WebSocket, REST for everything else | Don't over-engineer; sockets only where needed |
| Performance: FlashList + expo-image + Reanimated | 60fps even with complex UIs |

* * *

## Conclusion

The apps we admire didn't start with perfect architecture. They evolved. Engineers at Instagram and WhatsApp have written public post-mortems about the painful rewrites they had to do when their early structure couldn't scale. You have the advantage of learning from those rewrites.

Expo Router gives you a routing system that grows with you. Feature-based architecture gives you a codebase where every file has a home. A layered state strategy gives you the right tool for every type of data.

You don't need to build Instagram. You need to build *your* app in a way that won't embarrass you in two years. Start with structure. The features follow naturally.
