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',
offlineCa...
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.