When I joined KryptoMind LLC to lead the development of Cryptokara, a decentralized cryptocurrency wallet, I didn’t anticipate the scale we’d eventually reach. Today, with over 500,000 active users managing millions in crypto transactions daily, I want to share the architectural decisions, challenges, and lessons learned that made this scale possible.
The Challenge: Building Trust at Scale
Building a cryptocurrency wallet isn’t just about moving numbers between addresses. You’re asking users to trust you with their life savings. One bug, one security flaw, one moment of downtime could mean financial ruin for thousands. This reality shaped every decision we made.
Architecture: Mobile-First with React Native
We chose React Native for several strategic reasons:
- Single Codebase, Dual Platform: With limited resources, maintaining separate iOS and Android apps wasn’t feasible
- Performance: For a wallet app, 60 FPS scrolling through transaction history isn’t negotiable
- Native Modules: Direct access to secure storage and biometric authentication
Key Technical Decisions
Offline-First Architecture
// We implemented a robust offline queue system
class TransactionQueue {
private queue: Transaction[] = [];
async queueTransaction(tx: Transaction) {
await AsyncStorage.setItem(`pending_tx_${tx.id}`, JSON.stringify(tx));
this.queue.push(tx);
}
async processQueue() {
// Only process when online and synced
if (!this.isOnline() || !this.isSynced()) return;
for (const tx of this.queue) {
try {
await this.broadcastTransaction(tx);
await this.removeFromQueue(tx.id);
} catch (error) {
// Retry logic with exponential backoff
await this.scheduleRetry(tx);
}
}
}
}
This approach meant users could create transactions during network issues, and we’d broadcast them when connectivity returned.
Security: Non-Negotiable from Day One
Private Key Management
We never stored private keys on our servers. Period. Instead:
// Keys encrypted with user's PIN using AES-256
const encryptPrivateKey = async (privateKey: string, pin: string) => {
const salt = await generateSalt();
const key = await deriveKey(pin, salt);
return {
encrypted: await encrypt(privateKey, key),
salt: salt.toString("hex"),
algorithm: "aes-256-gcm",
};
};
Biometric Authentication
We implemented multiple layers:
- Face ID / Fingerprint for app access
- Additional PIN for transactions over $1,000
- Hardware security module integration for high-value accounts
Scaling Challenges We Faced
1. Real-Time Price Updates
With 500K users checking prices simultaneously, polling APIs wasn’t sustainable.
Solution: WebSocket connections with intelligent batching
class PriceUpdateManager {
private connections = new Map<string, WebSocket[]>();
broadcastPriceUpdate(currency: string, price: number) {
const sockets = this.connections.get(currency) || [];
// Batch updates every 2 seconds
const batch = {
currency,
price,
timestamp: Date.now(),
// Include multiple currencies in one message
batch: this.getPendingUpdates(),
};
sockets.forEach((socket) => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(batch));
}
});
}
}
2. Transaction History Pagination
Loading 10,000+ transactions crashed the app. We implemented:
- Virtual scrolling with
react-native-virtualized-list - Progressive loading (50 transactions at a time)
- Indexed database queries in MongoDB
// Optimized MongoDB query with proper indexing
db.transactions.createIndex({
userId: 1,
timestamp: -1,
});
// Query with cursor-based pagination
const getTransactions = async (userId, cursor, limit = 50) => {
const query = { userId };
if (cursor) {
query.timestamp = { $lt: cursor };
}
return db.transactions
.find(query)
.sort({ timestamp: -1 })
.limit(limit)
.toArray();
};
3. Multi-Chain Support
Supporting Bitcoin, Ethereum, BSC, and TRON meant different:
- Transaction formats
- Gas fee calculations
- Block confirmation times
- Wallet derivation paths
We abstracted this with a chain adapter pattern:
interface ChainAdapter {
createWallet(mnemonic: string): Promise<Wallet>;
getBalance(address: string): Promise<string>;
estimateFee(transaction: Transaction): Promise<string>;
broadcastTransaction(signed: SignedTransaction): Promise<string>;
}
class EthereumAdapter implements ChainAdapter {
async estimateFee(transaction: Transaction): Promise<string> {
const gasPrice = await this.provider.getGasPrice();
const gasLimit = await this.estimateGas(transaction);
return gasPrice.mul(gasLimit).toString();
}
}
Performance Optimizations That Mattered
1. Lazy Loading & Code Splitting
// Don't load staking module until user opens staking
const StakingScreen = lazy(() => import("./screens/Staking"));
// Preload high-priority screens during splash
const preloadScreens = async () => {
await Promise.all([
import("./screens/Wallet"),
import("./screens/Send"),
import("./screens/Receive"),
]);
};
2. Image Caching for Token Icons
// Custom cache with expiry
const iconCache = new Map<string, CachedImage>();
const getCachedIcon = async (tokenId: string) => {
const cached = iconCache.get(tokenId);
if (cached && Date.now() - cached.timestamp < 86400000) {
return cached.data;
}
const image = await fetchTokenIcon(tokenId);
iconCache.set(tokenId, {
data: image,
timestamp: Date.now(),
});
return image;
};
3. Background Sync with Expo Task Manager
TaskManager.defineTask(BACKGROUND_SYNC, async () => {
try {
// Sync pending transactions
await transactionQueue.processQueue();
// Update balances
await balanceService.syncBalances();
// Check for incoming transactions
await notificationService.checkIncoming();
return BackgroundFetch.Result.NewData;
} catch (error) {
return BackgroundFetch.Result.Failed;
}
});
Database Design for Scale
MongoDB was perfect for our use case:
// Efficient schema design
{
_id: ObjectId,
userId: String, // indexed
wallets: [{
address: String,
chain: String,
balance: Decimal128, // Precise decimal storage
lastSynced: Date
}],
transactions: { // Separate collection
// Reference by userId
},
preferences: {
currency: String,
notifications: Boolean
}
}
Sharding Strategy
// Shard by userId for even distribution
sh.shardCollection("cryptokara.users", { userId: 1 });
sh.shardCollection("cryptokara.transactions", { userId: 1, timestamp: -1 });
Monitoring & Observability
We used:
- Sentry for crash reporting (99.9% crash-free rate)
- Custom metrics for transaction success rates
- MongoDB Atlas monitoring for database performance
// Track critical metrics
const recordTransaction = async (tx: Transaction) => {
const startTime = Date.now();
try {
await broadcastTransaction(tx);
metrics.increment("transactions.success", {
chain: tx.chain,
amount_range: getAmountRange(tx.amount),
});
} catch (error) {
metrics.increment("transactions.failed", {
chain: tx.chain,
error: error.code,
});
throw error;
} finally {
metrics.timing("transactions.duration", Date.now() - startTime);
}
};
Key Takeaways
- Security First: Never compromise on security for features
- Offline-First: Mobile apps must work without internet
- Performance: 60 FPS isn’t optional for financial apps
- Monitoring: You can’t fix what you can’t measure
- Gradual Rollout: Use feature flags for risky changes
- User Trust: One bad transaction experience can lose a user forever
The Results
- 500K+ active users
- 99.9% uptime
- 4.5+ app store rating
- <100ms average transaction creation time
- Zero security breaches
Building Cryptokara taught me that scaling isn’t just about infrastructure—it’s about building trust, one secure transaction at a time.
Have questions about building scalable mobile applications? Feel free to reach out. I’m always happy to share more detailed insights.