This content originally appeared on DEV Community and was authored by Nilava Chowdhury
Engineering a React Native app that handles millions of daily transactions
Introduction
When we launched udChalo’s mobile app for armed forces personnel, we knew we were building for a unique audience with specific needs: reliability in low-connectivity areas, security for sensitive data, and performance that matches native apps. This post shares our journey from zero to 100K+ users in 6 months, achieving a 4.5-star rating while processing millions of flight bookings.
The Scale Challenge
Requirements and Constraints
const appRequirements = {
scale: {
targetUsers: 100000,
dailyActiveUsers: 50000,
peakConcurrent: 10000,
transactionsPerDay: 100000,
dataSync: '5GB daily'
},
performance: {
coldStart: '<2 seconds',
navigation: '<100ms',
apiResponse: '<500ms',
offlineCapability: 'Full feature parity',
memoryUsage: '<150MB'
},
userEnvironment: {
devices: '70% Android, 30% iOS',
androidVersions: '60% < Android 8',
networkQuality: '40% on 2G/3G',
locations: 'Remote military bases',
storage: 'Limited (2-4GB available)'
},
security: {
encryption: 'Military-grade',
authentication: 'Biometric + PIN',
dataResidency: 'India only',
compliance: 'Government standards'
}
};
Architecture Decisions
React Native Architecture
// App Architecture
const architecture = {
core: {
version: 'React Native 0.71',
newArchitecture: true, // Fabric + TurboModules
hermes: true, // For better performance
reanimated: 3, // For 60fps animations
},
stateManagement: {
global: 'Redux Toolkit + RTK Query',
local: 'React Hook Form',
persistence: 'Redux Persist + MMKV',
},
navigation: {
library: 'React Navigation 6',
structure: 'Tab + Stack hybrid',
deepLinking: true,
},
native: {
modules: [
'Biometric Authentication',
'Secure Storage',
'Network Detection',
'Background Sync',
'Push Notifications'
],
}
};
// Folder Structure for Scale
const projectStructure = `
src/
βββ components/ # Shared components
β βββ atoms/ # Basic building blocks
β βββ molecules/ # Composite components
β βββ organisms/ # Complex components
βββ screens/ # Screen components
βββ navigation/ # Navigation configuration
βββ services/ # API and external services
βββ store/ # Redux store and slices
βββ utils/ # Utility functions
βββ hooks/ # Custom hooks
βββ native/ # Native module bridges
βββ constants/ # App constants
`;
Performance Optimization Strategy
// Performance-First Component Design
import React, { memo, useMemo, useCallback } from 'react';
import { FlatList, View, Text } from 'react-native';
import FastImage from 'react-native-fast-image';
interface FlightListProps {
flights: Flight[];
onSelect: (flight: Flight) => void;
}
// Optimized Flight List Component
export const FlightList = memo<FlightListProps>(({ flights, onSelect }) => {
// Memoize expensive calculations
const sortedFlights = useMemo(() =>
flights.sort((a, b) => a.price - b.price),
[flights]
);
// Optimize callbacks
const renderItem = useCallback(({ item }: { item: Flight }) => (
<FlightCard flight={item} onPress={() => onSelect(item)} />
), [onSelect]);
const keyExtractor = useCallback((item: Flight) => item.id, []);
const getItemLayout = useCallback((data: any, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}), []);
return (
<FlatList
data={sortedFlights}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
// Performance optimizations
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={10}
// Memory optimization
maintainVisibleContentPosition={{
minIndexForVisible: 0,
}}
/>
);
});
// Optimized Image Component
const FlightAirlineLogo = memo(({ airline }: { airline: string }) => {
const imageSource = useMemo(() => ({
uri: getAirlineLogoUrl(airline),
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}), [airline]);
return (
<FastImage
style={styles.airlineLogo}
source={imageSource}
resizeMode={FastImage.resizeMode.contain}
/>
);
});
Native Module Implementation
Biometric Authentication Module
// Android Native Module - BiometricAuthModule.java
package com.udchalo.biometric;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.FragmentActivity;
import com.facebook.react.bridge.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class BiometricAuthModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
private final Executor executor = Executors.newSingleThreadExecutor();
public BiometricAuthModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "BiometricAuth";
}
@ReactMethod
public void authenticate(String reason, Promise promise) {
FragmentActivity activity = (FragmentActivity) getCurrentActivity();
if (activity == null) {
promise.reject("E_ACTIVITY_NULL", "Activity is null");
return;
}
activity.runOnUiThread(() -> {
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Authentication Required")
.setSubtitle(reason)
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build();
BiometricPrompt biometricPrompt = new BiometricPrompt(
activity,
executor,
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
promise.resolve(true);
}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
promise.reject("E_AUTH_FAILED", errString.toString());
}
@Override
public void onAuthenticationFailed() {
promise.reject("E_AUTH_FAILED", "Authentication failed");
}
}
);
biometricPrompt.authenticate(promptInfo);
});
}
@ReactMethod
public void isAvailable(Promise promise) {
BiometricManager biometricManager = BiometricManager.from(reactContext);
switch (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
case BiometricManager.BIOMETRIC_SUCCESS:
promise.resolve(true);
break;
default:
promise.resolve(false);
break;
}
}
}
// iOS Native Module - BiometricAuth.swift
import LocalAuthentication
import React
@objc(BiometricAuth)
class BiometricAuth: NSObject {
@objc(authenticate:resolver:rejecter:)
func authenticate(reason: String,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock) {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
rejecter("E_BIOMETRIC_NOT_AVAILABLE", "Biometric authentication not available", error)
return
}
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason) { success, error in
DispatchQueue.main.async {
if success {
resolver(true)
} else {
rejecter("E_AUTH_FAILED", "Authentication failed", error)
}
}
}
}
@objc(isAvailable:rejecter:)
func isAvailable(resolver: RCTPromiseResolveBlock,
rejecter: RCTPromiseRejectBlock) {
let context = LAContext()
var error: NSError?
let available = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
resolver(available)
}
}
Background Sync Implementation
// Background Sync Manager
import BackgroundFetch from 'react-native-background-fetch';
import NetInfo from '@react-native-community/netinfo';
import { MMKV } from 'react-native-mmkv';
class BackgroundSyncManager {
private storage = new MMKV();
private syncQueue: SyncTask[] = [];
async initialize() {
await BackgroundFetch.configure({
minimumFetchInterval: 15, // 15 minutes
forceAlarmManager: false,
stopOnTerminate: false,
startOnBoot: true,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY
}, async (taskId) => {
await this.performSync();
BackgroundFetch.finish(taskId);
}, (error) => {
console.error('Background fetch failed:', error);
});
// Check for pending sync on app launch
await this.checkPendingSync();
}
async queueForSync(data: any, type: SyncType) {
const task: SyncTask = {
id: generateUUID(),
type,
data,
timestamp: Date.now(),
attempts: 0,
status: 'pending'
};
// Store in persistent queue
const queue = this.getSyncQueue();
queue.push(task);
this.storage.set('syncQueue', JSON.stringify(queue));
// Try immediate sync if online
const netInfo = await NetInfo.fetch();
if (netInfo.isConnected) {
await this.performSync();
}
}
async performSync() {
const queue = this.getSyncQueue();
const netInfo = await NetInfo.fetch();
if (!netInfo.isConnected || queue.length === 0) {
return;
}
const pendingTasks = queue.filter(task => task.status === 'pending');
for (const task of pendingTasks) {
try {
await this.syncTask(task);
task.status = 'completed';
} catch (error) {
task.attempts++;
task.lastError = error.message;
if (task.attempts >= 3) {
task.status = 'failed';
}
}
}
// Update queue
this.storage.set('syncQueue', JSON.stringify(queue));
// Clean old completed tasks
this.cleanupQueue();
}
private async syncTask(task: SyncTask) {
switch (task.type) {
case 'booking':
return await this.syncBooking(task.data);
case 'profile':
return await this.syncProfile(task.data);
case 'preferences':
return await this.syncPreferences(task.data);
default:
throw new Error(`Unknown sync type: ${task.type}`);
}
}
private getSyncQueue(): SyncTask[] {
const queueString = this.storage.getString('syncQueue');
return queueString ? JSON.parse(queueString) : [];
}
}
State Management at Scale
Redux Toolkit Setup
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { persistStore, persistReducer } from 'redux-persist';
import { MMKV } from 'react-native-mmkv';
// Custom MMKV storage adapter for Redux Persist
const storage = new MMKV();
const MMKVStorage = {
setItem: (key: string, value: string) => {
storage.set(key, value);
return Promise.resolve(true);
},
getItem: (key: string) => {
const value = storage.getString(key);
return Promise.resolve(value);
},
removeItem: (key: string) => {
storage.delete(key);
return Promise.resolve();
},
};
// RTK Query API
const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: Config.API_BASE_URL,
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) {
headers.set('authorization', `Bearer ${token}`);
}
return headers;
},
}),
tagTypes: ['Flight', 'Booking', 'User'],
endpoints: (builder) => ({
searchFlights: builder.query<Flight[], FlightSearchParams>({
query: (params) => ({
url: '/flights/search',
params,
}),
providesTags: ['Flight'],
// Cache for 5 minutes
keepUnusedDataFor: 300,
}),
createBooking: builder.mutation<Booking, CreateBookingDto>({
query: (booking) => ({
url: '/bookings',
method: 'POST',
body: booking,
}),
invalidatesTags: ['Booking'],
// Optimistic update
async onQueryStarted(booking, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
apiSlice.util.updateQueryData('getUserBookings', undefined, (draft) => {
draft.unshift(booking as any);
})
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
},
}),
}),
});
// Configure store with persistence
const persistConfig = {
key: 'root',
storage: MMKVStorage,
whitelist: ['auth', 'user', 'preferences'],
blacklist: ['api'], // Don't persist API cache
};
const rootReducer = combineReducers({
[apiSlice.reducerPath]: apiSlice.reducer,
auth: authSlice.reducer,
user: userSlice.reducer,
preferences: preferencesSlice.reducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(apiSlice.middleware),
});
setupListeners(store.dispatch);
export const persistor = persistStore(store);
Performance Monitoring
Custom Performance Metrics
// Performance Monitor
import performance from 'react-native-performance';
import analytics from '@react-native-firebase/analytics';
class PerformanceMonitor {
private marks = new Map<string, number>();
private measures = new Map<string, number[]>();
startMark(name: string) {
this.marks.set(name, performance.now());
}
endMark(name: string, customProperties?: Record<string, any>) {
const startTime = this.marks.get(name);
if (!startTime) return;
const duration = performance.now() - startTime;
// Store measure
if (!this.measures.has(name)) {
this.measures.set(name, []);
}
this.measures.get(name)!.push(duration);
// Send to analytics if significant
if (duration > this.getThreshold(name)) {
analytics().logEvent('performance_metric', {
metric_name: name,
duration,
...customProperties,
});
}
// Clean up
this.marks.delete(name);
}
private getThreshold(name: string): number {
const thresholds: Record<string, number> = {
app_launch: 2000,
screen_transition: 500,
api_call: 1000,
image_load: 500,
list_render: 100,
};
return thresholds[name] || 1000;
}
reportMetrics() {
const report: Record<string, any> = {};
this.measures.forEach((durations, name) => {
report[name] = {
avg: durations.reduce((a, b) => a + b, 0) / durations.length,
min: Math.min(...durations),
max: Math.max(...durations),
p95: this.calculatePercentile(durations, 0.95),
count: durations.length,
};
});
// Send to monitoring service
analytics().logEvent('performance_report', report);
// Clear old data
this.measures.clear();
}
private calculatePercentile(values: number[], percentile: number): number {
const sorted = values.sort((a, b) => a - b);
const index = Math.ceil(sorted.length * percentile) - 1;
return sorted[index];
}
}
Offline-First Architecture
Offline Data Sync
// Offline Manager
import { MMKV } from 'react-native-mmkv';
import NetInfo from '@react-native-community/netinfo';
class OfflineManager {
private storage = new MMKV({ id: 'offline-data' });
private syncInProgress = false;
async saveOfflineData(key: string, data: any) {
const offlineData = {
data,
timestamp: Date.now(),
synced: false,
};
this.storage.set(key, JSON.stringify(offlineData));
// Queue for sync
await this.queueForSync(key);
}
async getOfflineData(key: string): Promise<any> {
const stored = this.storage.getString(key);
if (!stored) return null;
const { data, timestamp, synced } = JSON.parse(stored);
// Check if data is stale
const isStale = Date.now() - timestamp > 24 * 60 * 60 * 1000; // 24 hours
if (isStale && !synced) {
// Try to sync
await this.syncSingleItem(key);
}
return data;
}
async syncOfflineData() {
if (this.syncInProgress) return;
const netInfo = await NetInfo.fetch();
if (!netInfo.isConnected) return;
this.syncInProgress = true;
try {
const allKeys = this.storage.getAllKeys();
const unsyncedKeys = allKeys.filter(key => {
const data = this.storage.getString(key);
if (!data) return false;
const parsed = JSON.parse(data);
return !parsed.synced;
});
for (const key of unsyncedKeys) {
await this.syncSingleItem(key);
}
} finally {
this.syncInProgress = false;
}
}
private async syncSingleItem(key: string) {
const stored = this.storage.getString(key);
if (!stored) return;
const { data, timestamp } = JSON.parse(stored);
try {
// Determine sync endpoint based on key pattern
const endpoint = this.getEndpointForKey(key);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
data,
clientTimestamp: timestamp,
}),
});
if (response.ok) {
// Mark as synced
const updated = {
data,
timestamp,
synced: true,
syncedAt: Date.now(),
};
this.storage.set(key, JSON.stringify(updated));
}
} catch (error) {
console.error(`Failed to sync ${key}:`, error);
}
}
}
Crash Reporting and Analytics
Crash Handler Setup
// Crash Reporter
import crashlytics from '@react-native-firebase/crashlytics';
import { ErrorBoundary } from 'react-error-boundary';
// Global error handler
ErrorUtils.setGlobalHandler((error: Error, isFatal: boolean) => {
// Log to Crashlytics
crashlytics().recordError(error, {
isFatal,
timestamp: Date.now(),
userId: getCurrentUserId(),
sessionId: getSessionId(),
});
// Log locally for debugging
if (__DEV__) {
console.error('Global error:', error);
}
// Show user-friendly error screen for fatal errors
if (isFatal) {
showErrorScreen(error);
}
});
// React Error Boundary
function AppErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
crashlytics().log('React error boundary triggered');
crashlytics().recordError(error, {
errorBoundary: true,
componentStack: errorInfo.componentStack,
});
}}
onReset={() => {
// Clear cache and restart app
clearAppCache();
RNRestart.Restart();
}}
>
{children}
</ErrorBoundary>
);
}
// Custom crash reporter for specific scenarios
class CrashReporter {
static logError(error: Error, context?: Record<string, any>) {
const enrichedContext = {
...context,
timestamp: Date.now(),
appVersion: DeviceInfo.getVersion(),
buildNumber: DeviceInfo.getBuildNumber(),
device: {
brand: DeviceInfo.getBrand(),
model: DeviceInfo.getModel(),
os: Platform.OS,
osVersion: DeviceInfo.getSystemVersion(),
},
memory: {
used: DeviceInfo.getUsedMemorySync(),
total: DeviceInfo.getTotalMemorySync(),
},
network: getNetworkState(),
};
crashlytics().recordError(error, enrichedContext);
// Send to custom monitoring
if (shouldSendToCustomMonitoring(error)) {
sendToCustomMonitoring(error, enrichedContext);
}
}
}
Testing Strategy
E2E Testing with Detox
// e2e/bookingFlow.test.js
describe('Booking Flow', () => {
beforeAll(async () => {
await device.launchApp({
newInstance: true,
permissions: { notifications: 'YES', location: 'always' },
});
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should complete flight booking successfully', async () => {
// Login
await element(by.id('email-input')).typeText('test@udchalo.com');
await element(by.id('password-input')).typeText('Test@1234');
await element(by.id('login-button')).tap();
// Search flights
await waitFor(element(by.id('search-screen')))
.toBeVisible()
.withTimeout(5000);
await element(by.id('from-airport')).tap();
await element(by.text('Delhi (DEL)')).tap();
await element(by.id('to-airport')).tap();
await element(by.text('Mumbai (BOM)')).tap();
await element(by.id('departure-date')).tap();
await element(by.text('15')).tap();
await element(by.id('search-button')).tap();
// Select flight
await waitFor(element(by.id('flight-list')))
.toBeVisible()
.withTimeout(10000);
await element(by.id('flight-item-0')).tap();
// Passenger details
await element(by.id('passenger-name')).typeText('John Doe');
await element(by.id('passenger-phone')).typeText('9876543210');
await element(by.id('continue-button')).tap();
// Payment
await element(by.id('payment-method-upi')).tap();
await element(by.id('upi-id')).typeText('test@upi');
await element(by.id('pay-button')).tap();
// Verify booking success
await waitFor(element(by.id('booking-success')))
.toBeVisible()
.withTimeout(15000);
await expect(element(by.id('booking-id'))).toBeVisible();
});
it('should handle network failure gracefully', async () => {
// Disable network
await device.setURLBlacklist(['.*']);
// Try to search flights
await element(by.id('search-button')).tap();
// Should show offline message
await expect(element(by.text('You are offline'))).toBeVisible();
// Re-enable network
await device.clearURLBlacklist();
// Should auto-retry
await waitFor(element(by.id('flight-list')))
.toBeVisible()
.withTimeout(10000);
});
});
Production Metrics and Results
const productionMetrics = {
scale: {
totalUsers: 127000,
dailyActiveUsers: 52000,
monthlyActiveUsers: 98000,
peakConcurrentUsers: 12500,
dailyTransactions: 85000,
totalBookings: 15000000 // Total value
},
performance: {
appStartTime: {
cold: 1.8, // seconds
warm: 0.6
},
screenTransition: 92, // milliseconds
apiResponseTime: {
p50: 145,
p95: 380,
p99: 520
},
crashFreeRate: 99.7, // percentage
anr: 0.02 // percentage
},
userEngagement: {
avgSessionLength: '8:30', // minutes
dailySessions: 3.2,
retention: {
day1: 82,
day7: 68,
day30: 54
},
appStoreRating: 4.5,
playStoreRating: 4.4
},
technical: {
bundleSize: {
android: '28MB',
ios: '32MB'
},
memoryUsage: {
avg: 95, // MB
peak: 142
},
batteryImpact: 'Low',
offlineCapability: '100% core features'
}
};
Key Learnings
- Performance is a Feature: Users expect native-like performance from React Native
- Offline-First is Essential: Military bases often have poor connectivity
- Native Modules are Powerful: Don’t hesitate to write native code when needed
- State Management Matters: Redux Toolkit + RTK Query simplified complex state
- Monitoring is Crucial: Can’t improve what you don’t measure
Conclusion
Building a React Native app that scales to 100K+ users taught us that success lies in making the right architectural decisions early, obsessing over performance, and never compromising on user experience. The combination of React Native’s cross-platform benefits with strategic native optimizations enabled us to deliver an app that serves critical needs of armed forces personnel reliably, even in the most challenging conditions.
This content originally appeared on DEV Community and was authored by Nilava Chowdhury