Architecture Patterns That Scale
Battle-tested patterns from applications serving millions of users
You've decided React Native fits your needs. Now the critical question: how do you architect an application that scales from MVP to enterprise without costly rewrites?
These aren't theoretical best practices — they're lessons learned from production deployments at scale.
The Foundation: Project Structure
Monorepo (Recommended for most teams)
/packages
/mobile # React Native app
/web # React web app (optional)
/shared # Shared business logic, types
/ui # Shared component library
/apps
/ios # iOS-specific native modules
/android # Android-specific native modules
Why monorepo wins in 2026:
- Expo's monorepo support is mature
- Shared code between web and mobile is seamless
- Single CI/CD pipeline for all platforms
- Atomic commits across packages
Feature-Based Architecture
/src
/features
/auth
/components
/hooks
/screens
/services
/store
index.ts
/profile
/payments
/shared
/components
/hooks
/services
/navigation
/app
Why this matters: Features are self-contained and testable. Teams can work independently. Clear ownership boundaries.
State Management in 2026
Tier 1: Server State (TanStack Query)
Use for: All data that lives on a server
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
export function useProducts(categoryId: string) {
return useQuery({
queryKey: ["products", categoryId],
queryFn: () => productService.getByCategory(categoryId),
staleTime: 5 * 60 * 1000,
});
}
export function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: productService.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["products"] });
},
});
}
Why TanStack Query dominates:
- Automatic caching, refetching, synchronization
- Optimistic updates out of the box
- 90% of "state" in most apps is server state
Tier 2: Global Client State (Zustand)
Use for: App-wide client state (preferences, UI state, feature flags)
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface AppState {
theme: "light" | "dark" | "system";
setTheme: (theme: AppState["theme"]) => void;
}
export const useAppStore = create<AppState>()(
persist(
(set) => ({
theme: "system",
setTheme: (theme) => set({ theme }),
}),
{ name: "app-storage" }
)
);
Why Zustand over Redux in 2026:
- Zero boilerplate
- TypeScript-first
- Built-in persistence
- No providers needed
Tier 3: Local Component State
Just use useState and useReducer for UI state that doesn't need sharing.
Navigation Architecture
Expo Router (Recommended)
File-based routing, similar to Next.js:
/app
_layout.tsx
index.tsx
/auth
login.tsx
register.tsx
/(tabs)
_layout.tsx
profile.tsx
/product
[id].tsx
Why Expo Router wins:
- Deep linking works automatically
- Type-safe routes
- Familiar to web developers
API Layer Architecture
Service Pattern
// Base client with interceptors
export const apiClient = axios.create({
baseURL: process.env.EXPO_PUBLIC_API_URL,
timeout: 10000,
});
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().accessToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Feature service
export const productService = {
getAll: async (): Promise<Product[]> => {
const { data } = await apiClient.get("/products");
return data;
},
create: async (dto: CreateProductDTO): Promise<Product> => {
const { data } = await apiClient.post("/products", dto);
return data;
},
};
Performance Patterns
Use FlashList for Large Lists
import { FlashList } from "@shopify/flash-list";
<FlashList
data={products}
renderItem={({ item }) => <ProductCard product={item} />}
estimatedItemSize={120}
/>
Image Optimization with expo-image
import { Image } from "expo-image";
<Image
source={{ uri: product.imageUrl }}
contentFit="cover"
placeholder={blurhash}
transition={200}
/>
Memoization Strategy
// Memoize expensive computations
const sortedProducts = useMemo(
() => products.sort((a, b) => b.rating - a.rating),
[products]
);
// Memoize callbacks
const handlePress = useCallback((id: string) => {
navigation.navigate("ProductDetail", { productId: id });
}, [navigation]);
Testing Architecture
The Testing Pyramid
/\
/ E2E \ 10% (Detox/Maestro)
/________\
/ Integration \ 30% (RNTL)
/______________\
/ Unit \ 60% (Jest)
/____________________\
Key Takeaways
- Start with the right structure — Monorepo + feature-based architecture
- Layer your state — Server (TanStack Query) → Global (Zustand) → Local (useState)
- Type everything — TypeScript catches bugs before runtime
- Optimize later — Profile first, don't premature optimize
- Test the pyramid — More unit tests, fewer E2E tests
Next in series: Team Composition & Hiring Strategies
About the author: Mobile architect with 10+ years building applications at scale. Currently at Lotus Innovations.
Top comments (0)