Frontend System Design
Design a real-time dashboard handling millions of events, thousands of concurrent users, and live updates without impacting performance.
Functional
Non-Functional
Backend APIs
REST / GraphQL
Initial data, user management, historical queries
WebSocket Server
Real-time updates
Live metrics push, alert notifications, presence
Frontend Dashboard
React Application
State management, rendering, caching, charts, tables
REST/GraphQL handles initial data loads and mutations. WebSocket carries live updates. The frontend maintains a unified state store fed by both channels — never duplicate sources of truth.
src/ pages/ # Route-based pages components/ # Reusable components features/ # Feature-based modules services/ # API calls, clients hooks/ # Custom React hooks store/ # Global state (Redux/Zustand) utils/ # Utility functions layouts/ # Layout components charts/ # Chart components websocket/ # WebSocket logic
Feature-based modules own their own components, hooks, and local state — reducing cross-feature coupling.
Dashboard
├─ Header
├─ Sidebar
├─ MetricsSection
│ ├─ RevenueCard
│ ├─ UsersCard
│ └─ OrdersCard
├─ ChartsSection
│ ├─ RevenueChart
│ ├─ TrafficChart
│ └─ CPUChart
├─ AlertsPanel
└─ ActivityFeed
Global Client State — Redux / Zustand
// Zustand store slice
const useUIStore = create((set) => ({
webSocketStatus: 'disconnected',
alerts: [],
setStatus: (s) => set({ webSocketStatus: s }),
addAlert: (a) =>
set((state) => ({ alerts: [...state.alerts, a] })),
}));Server State — TanStack Query
const { data } = useQuery({
queryKey: ['dashboard', userId],
queryFn: fetchDashboard,
staleTime: 5_000,
refetchInterval: 30_000,
});Each chart component opens its own WebSocket. Results in high overhead, race conditions, and dropped messages.
// ❌ Each component connects independently
function RevenueChart() {
const ws = new WebSocket('/ws');
ws.onmessage = (e) => setData(parse(e));
}
function TrafficChart() {
const ws = new WebSocket('/ws'); // duplicate!
}One singleton WebSocket service. Distributes events to the state store; components subscribe only to what they need.
// ✓ Singleton service, fan-out via store
class WebSocketService {
connect() {
this.ws = new WebSocket('/ws');
this.ws.onmessage = ({ data }) => {
const event = JSON.parse(data);
store.dispatch(handleLiveEvent(event));
};
}
}// ❌ 1000 dispatches/sec → UI freezes
socket.on('metric', (data) => {
dispatch(updateMetric(data)); // every single event
});// ✓ Buffer updates, flush every 1 sec
const buffer = [];
socket.on('metric', (data) => {
buffer.push(data);
});
setInterval(() => {
if (buffer.length === 0) return;
dispatch(batchUpdateMetrics(buffer.splice(0)));
}, 1000); // one re-render per secondBatching reduces React reconciliations from 1000/sec to 1/sec. Pair with startTransition to keep interactions responsive while background updates are pending.
React.memoSkip re-renders for stable propsuseMemoMemoize expensive calculationsuseCallbackStable function references for childrenCode Splittinglazy() + Suspense per route/featureVirtualizationOnly render visible rows/cellsKey propsStable keys prevent full subtree remounts// Avoid inline functions — new ref every render
// ❌ <Chart onHover={() => setHovered(id)} />
// ✓
const handleHover = useCallback(
() => setHovered(id), [id]
);
<Chart onHover={handleHover} />Charts
Tables
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={600}
itemCount={60_000}
itemSize={40}
width="100%"
>
{({ index, style }) => (
<Row style={style} data={rows[index]} />
)}
</FixedSizeList>Initial Load — Parallel Requests
// Fire all requests in parallel, not waterfall
const [summary, settings, history, filters] =
await Promise.all([
fetchDashboardSummary(),
fetchUserSettings(),
fetchHistoricalData({ range: '7d' }),
fetchFilterOptions(),
]);Caching Strategy
5 secLive metrics, alerts30 secUser counts, summaries60 secConfig, filter optionsSet staleTimeper query key — don't apply a single TTL globally.
WebSocket Connection States
Exponential Backoff Reconnect
1s → 2s → 4s → 8s → 16s → …
function reconnect(attempt = 0) {
const delay = Math.min(1000 * 2 ** attempt, 30_000);
setTimeout(() => {
ws = new WebSocket(WS_URL);
ws.onerror = () => reconnect(attempt + 1);
ws.onopen = () => { attempt = 0; }; // reset
}, delay);
}Show user-friendly status banners. Always expose a manual “Retry” button — never leave users staring at stale data silently.
Core Web Vitals
Track
Frontend Scaling
Backend / Platform Scaling
Goal
Handle 10× more data without compromising UX — each optimization must be measurable, not just theoretical.
Junior Engineer Asks
“How do I build this chart?”
Senior Engineer Asks
“How do I keep this dashboard responsive when it receives 1 million events per minute, serves 100,000 users, and continues to work during partial failures?”
Great dashboards are not built with more components, but with the right architecture, performance optimizations, and resilience patterns applied at every layer.