Architecting a Data-Intensive Investment Dashboard: Real-Time Analytics at Scale
The Challenge
The investor dashboard was the heart of Capitalino's platform. It needed to provide real-time portfolio analytics, transaction history, performance metrics, and investment insights—all while handling thousands of concurrent users and processing millions of data points.
The challenge wasn't just building a dashboard—it was building a system that could:
Architecture Overview
┌─────────────────────────────────────────┐
│ Client (React/Next.js) │
│ - Real-time updates via WebSocket │
│ - Data visualization │
│ - Interactive charts │
└──────────────┬──────────────────────────┘
│ WebSocket / REST API
▼
┌─────────────────────────────────────────┐
│ API Gateway (Nginx) │
│ - Rate limiting │
│ - Load balancing │
└──────────────┬──────────────────────────┘
│
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Analytics│ │Portfolio│ │Transaction│
│Service │ │Service │ │Service │
└────────┘ └────────┘ └────────┘
│ │ │
└──────────┼──────────┘
│
▼
┌──────────┐
│ MongoDB │
│ Redis │
└──────────┘
Core Components
1. Portfolio Aggregation Service
class PortfolioService {
private redis: Redis;
private mongo: MongoClient;
async getPortfolioValue(userId: string): Promise {
// Check cache first
const cacheKey = portfolio:${userId};
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Aggregate from multiple sources
const [investments, crypto, fiat, stocks] = await Promise.all([
this.getInvestmentTotal(userId),
this.getCryptoBalance(userId),
this.getFiatBalance(userId),
this.getStockHoldings(userId)
]);
const total = investments + crypto + fiat + stocks.totalValue;
// Calculate performance metrics
const performance = await this.calculatePerformance(userId, total);
// Calculate allocation percentages
const allocation = {
investments: (investments / total) * 100,
crypto: (crypto / total) * 100,
fiat: (fiat / total) * 100,
stocks: (stocks.totalValue / total) * 100
};
const summary: PortfolioSummary = {
totalValue: total,
breakdown: { investments, crypto, fiat, stocks },
allocation,
performance,
lastUpdated: new Date()
};
// Cache for 30 seconds
await this.redis.setex(cacheKey, 30, JSON.stringify(summary));
return summary;
}
private async calculatePerformance(
userId: string,
currentValue: number
): Promise {
// Get historical data
const history = await this.mongo
.db('analytics')
.collection('portfolio_history')
.find({ userId })
.sort({ timestamp: -1 })
.limit(30)
.toArray();
if (history.length === 0) {
return {
dailyChange: 0,
dailyChangePercent: 0,
weeklyChange: 0,
weeklyChangePercent: 0,
monthlyChange: 0,
monthlyChangePercent: 0
};
}
const yesterday = history.find(h =>
isYesterday(new Date(h.timestamp))
)?.value || currentValue;
const lastWeek = history.find(h =>
isLastWeek(new Date(h.timestamp))
)?.value || currentValue;
const lastMonth = history[history.length - 1]?.value || currentValue;
return {
dailyChange: currentValue - yesterday,
dailyChangePercent: ((currentValue - yesterday) / yesterday) * 100,
weeklyChange: currentValue - lastWeek,
weeklyChangePercent: ((currentValue - lastWeek) / lastWeek) * 100,
monthlyChange: currentValue - lastMonth,
monthlyChangePercent: ((currentValue - lastMonth) / lastMonth) * 100
};
}
}
2. Real-Time Analytics Engine
class AnalyticsService {
async getRealTimeMetrics(userId: string): Promise {
// Get current market data
const marketData = await this.fetchMarketData();
// Get user positions
const positions = await this.getUserPositions(userId);
// Calculate real-time P&L
const unrealizedPnL = positions.reduce((total, position) => {
const currentPrice = marketData[position.symbol]?.price || 0;
const entryPrice = position.entryPrice;
const quantity = position.quantity;
return total + ((currentPrice - entryPrice) * quantity);
}, 0);
// Calculate exposure
const totalExposure = positions.reduce((total, position) => {
return total + (position.currentValue || 0);
}, 0);
return {
unrealizedPnL,
totalExposure,
positions: positions.length,
lastUpdate: new Date()
};
}
async generateInsights(userId: string): Promise {
const portfolio = await this.portfolioService.getPortfolioValue(userId);
const transactions = await this.getRecentTransactions(userId, 100);
const insights: InvestmentInsight[] = [];
// Diversification analysis
if (portfolio.allocation.crypto > 50) {
insights.push({
type: 'warning',
title: 'High Crypto Concentration',
message: 'Your portfolio is heavily weighted in cryptocurrency. Consider diversifying.',
priority: 'high'
});
}
// Performance insights
if (portfolio.performance.dailyChangePercent < -5) {
insights.push({
type: 'alert',
title: 'Significant Daily Loss',
message: Your portfolio decreased by ${portfolio.performance.dailyChangePercent.toFixed(2)}% today.,
priority: 'medium'
});
}
// Trading pattern analysis
const tradingFrequency = this.analyzeTradingPattern(transactions);
if (tradingFrequency > 10) {
insights.push({
type: 'info',
title: 'High Trading Activity',
message: 'You have made many trades recently. Consider a long-term strategy.',
priority: 'low'
});
}
return insights;
}
}
3. WebSocket Real-Time Updates
import { Server as SocketServer } from 'socket.io';class DashboardWebSocketService {
private io: SocketServer;
private portfolioService: PortfolioService;
private analyticsService: AnalyticsService;
constructor(io: SocketServer) {
this.io = io;
this.setupEventHandlers();
this.startPeriodicUpdates();
}
private setupEventHandlers() {
this.io.on('connection', (socket) => {
const userId = socket.handshake.auth.userId;
// Join user's room
socket.join(user:${userId});
// Send initial data
this.sendPortfolioUpdate(userId);
// Handle disconnection
socket.on('disconnect', () => {
console.log(User ${userId} disconnected);
});
});
}
private async sendPortfolioUpdate(userId: string) {
try {
const portfolio = await this.portfolioService.getPortfolioValue(userId);
const metrics = await this.analyticsService.getRealTimeMetrics(userId);
const insights = await this.analyticsService.generateInsights(userId);
this.io.to(user:${userId}).emit('portfolio:update', {
portfolio,
metrics,
insights,
timestamp: new Date()
});
} catch (error) {
console.error('Error sending portfolio update', error);
}
}
private startPeriodicUpdates() {
// Update every 5 seconds
setInterval(async () => {
const connectedUsers = await this.getConnectedUsers();
for (const userId of connectedUsers) {
await this.sendPortfolioUpdate(userId);
}
}, 5000);
}
}
4. Data Visualization Optimization
// Optimized chart data aggregation
class ChartDataService {
async getHistoricalData(
userId: string,
timeframe: '1D' | '1W' | '1M' | '1Y'
): Promise {
const cacheKey = chart:${userId}:${timeframe};
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Calculate time range
const now = new Date();
const startTime = this.getStartTime(timeframe, now);
// Aggregate data points
const rawData = await this.mongo
.db('analytics')
.collection('portfolio_history')
.find({
userId,
timestamp: { $gte: startTime, $lte: now }
})
.sort({ timestamp: 1 })
.toArray();
// Downsample for performance
const dataPoints = this.downsample(rawData, this.getSampleCount(timeframe));
// Cache for 1 minute
await redis.setex(cacheKey, 60, JSON.stringify(dataPoints));
return dataPoints;
}
private downsample(
data: any[],
targetCount: number
): ChartDataPoint[] {
if (data.length <= targetCount) {
return data;
}
const step = Math.ceil(data.length / targetCount);
const sampled: ChartDataPoint[] = [];
for (let i = 0; i < data.length; i += step) {
const chunk = data.slice(i, i + step);
const avgValue = chunk.reduce((sum, d) => sum + d.value, 0) / chunk.length;
sampled.push({
timestamp: chunk[0].timestamp,
value: avgValue
});
}
return sampled;
}
}
Performance Optimizations
Caching Strategy
Database Optimization
Frontend Optimization
Results
Metrics
User Impact
Lessons Learned
Conclusion
Building the investment dashboard was a masterclass in building data-intensive applications. It required careful architecture, performance optimization, and real-time data handling. The dashboard successfully serves thousands of users with sub-second response times and real-time updates.
The experience taught me that building financial dashboards is about more than just displaying data—it's about providing insights, enabling decisions, and creating trust through reliability and performance.
---
Interested in data-intensive applications, real-time systems, or financial technology? Let's connect!