Back to Blogs
Architecting a Data-Intensive Investment Dashboard: Real-Time Analytics at Scale

Architecting a Data-Intensive Investment Dashboard: Real-Time Analytics at Scale

12/18/202511 min
dashboardanalyticsreal-timenodejswebsocketdata-visualization

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:

  • Aggregate data from multiple sources in real-time

  • Perform complex calculations efficiently

  • Handle high concurrency

  • Provide sub-second response times

  • Scale horizontally as users grew
  • 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

  • Redis caching for frequently accessed data (30s-5min TTL)

  • In-memory caching for static reference data

  • CDN caching for static assets

  • Browser caching for chart libraries
  • Database Optimization

  • Indexed queries on userId, timestamp, and symbol

  • Read replicas for analytics queries

  • Connection pooling to handle high concurrency

  • Query result pagination for large datasets
  • Frontend Optimization

  • Virtual scrolling for transaction lists

  • Lazy loading for chart components

  • Debounced search to reduce API calls

  • Optimistic UI updates for better perceived performance
  • Results

    Metrics

  • Response Time: < 200ms for portfolio data

  • Real-Time Updates: 5-second refresh interval

  • Concurrent Users: 5,000+ simultaneous connections

  • Data Points Processed: 10M+ per day

  • Cache Hit Rate: 85%

  • Uptime: 99.9%
  • User Impact

  • User Satisfaction: 4.8/5.0 average rating

  • Daily Active Users: 3,000+

  • Average Session Time: 12 minutes

  • Feature Adoption: 80% of users use real-time features
  • Lessons Learned

  • Caching is critical - Aggressive caching reduced database load by 80%

  • Real-time is expensive - WebSocket connections require careful resource management

  • Data aggregation is complex - Multiple data sources need careful synchronization

  • Performance monitoring is essential - Real-time dashboards need comprehensive monitoring

  • User experience matters - Fast loading and smooth updates improve engagement
  • 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!