Frontend System Design
Frontend System Design
Tested in: Full-stack and senior FE interviews at CRED, Razorpay, Flipkart, Meesho. Format: "Design the frontend for X" — 30-45 min. Focus on architecture, not pixel design.
Interview Framework (use this structure every time)
1. Clarify requirements (5 min)
- Functional: what features exactly?
- Non-functional: scale, performance, accessibility, browser support
2. Component breakdown (5 min)
- Draw component tree
- Identify smart vs dumb components
3. Data flow (10 min)
- Where does data come from? (API, WebSocket, local state)
- State management: local vs global
- Caching strategy
4. API design (5 min)
- What endpoints do you need?
- Pagination? Real-time?
5. Performance (5 min)
- Lazy loading, code splitting, virtualization
- Network: debounce, throttle, cache
6. Edge cases + accessibility (5 min)
- Loading, error, empty states
- Keyboard navigation, ARIA
Problem 1: Typeahead / Autocomplete
Requirements: Search box that shows suggestions as user types.
Component tree:
SearchBox
├── TextInput
├── SuggestionList
│ └── SuggestionItem (×N)
└── LoadingSpinner
Data flow:
User types → debounce(300ms) → API call → show results
Cache: Map<query, results> in memory (avoid duplicate calls)
Key decisions:
1. Debounce (300ms) — don't hit API on every keystroke
2. Cancel previous request if new one fires (AbortController)
3. Min chars (2-3) before firing
4. Keyboard nav: ArrowUp/Down to select, Enter to submit, Esc to close
const cache = new Map<string, string[]>();
function useAutocomplete(query: string) {
const [results, setResults] = useState<string[]>([]);
const abortRef = useRef<AbortController>();
useEffect(() => {
if (query.length < 2) { setResults([]); return; }
if (cache.has(query)) { setResults(cache.get(query)!); return; }
abortRef.current?.abort();
abortRef.current = new AbortController();
fetch(`/api/suggest?q=${query}`, { signal: abortRef.current.signal })
.then(r => r.json())
.then(data => {
cache.set(query, data);
setResults(data);
})
.catch(e => { if (e.name !== 'AbortError') console.error(e); });
}, [query]);
return results;
}
Problem 2: Infinite Scroll Feed
Requirements: News/social feed that loads more items as user scrolls.
Component tree:
Feed
├── PostList
│ └── PostCard (×N) — virtualized
├── LoadingIndicator (at bottom)
└── ErrorState
Data flow:
Initial load → cursor-based pagination → IntersectionObserver triggers next page
Key decisions:
1. Cursor pagination (not offset) — consistent under new posts
2. Virtualization (react-window) — only render visible items, not 1000 DOM nodes
3. IntersectionObserver on sentinel div at bottom of list
4. Optimistic updates for likes/comments
function Feed() {
const [posts, setPosts] = useState([]);
const [cursor, setCursor] = useState(null);
const sentinelRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting && cursor !== null) loadMore();
});
if (sentinelRef.current) observer.observe(sentinelRef.current);
return () => observer.disconnect();
}, [cursor]);
async function loadMore() {
const { data, next_cursor } = await fetchPosts(cursor);
setPosts(prev => [...prev, ...data]);
setCursor(next_cursor);
}
return (
<div>
{posts.map(p => <PostCard key={p.id} post={p} />)}
<div ref={sentinelRef} /> {/* trigger element */}
</div>
);
}
Problem 3: Real-Time Dashboard (WebSocket)
Requirements: Live metrics dashboard (trading, monitoring, logistics).
Component tree:
Dashboard
├── MetricCard (×N) — subscribes to specific metrics
├── Chart (live updating)
└── ConnectionStatus
Data flow:
WebSocket connection → message dispatch → selective re-render
Key decisions:
1. Single WebSocket connection for all metrics (not one per widget)
2. Subscribe/unsubscribe pattern per widget
3. Reconnect with exponential backoff on disconnect
4. Buffer updates — batch DOM updates (requestAnimationFrame)
5. Stale-while-revalidate: show last known value while reconnecting
function useWebSocket(url: string) {
const [status, setStatus] = useState<'connecting'|'open'|'closed'>('connecting');
const wsRef = useRef<WebSocket>();
const listeners = useRef<Map<string, Set<Function>>>(new Map());
function connect() {
const ws = new WebSocket(url);
ws.onopen = () => setStatus('open');
ws.onclose = () => {
setStatus('closed');
setTimeout(connect, Math.min(1000 * 2 ** retries++, 30000)); // backoff
};
ws.onmessage = ({ data }) => {
const { type, payload } = JSON.parse(data);
listeners.current.get(type)?.forEach(fn => fn(payload));
};
wsRef.current = ws;
}
function subscribe(event: string, fn: Function) {
if (!listeners.current.has(event)) listeners.current.set(event, new Set());
listeners.current.get(event)!.add(fn);
return () => listeners.current.get(event)?.delete(fn); // unsubscribe
}
useEffect(() => { connect(); return () => wsRef.current?.close(); }, []);
return { status, subscribe };
}
Problem 4: Form with Complex Validation
Requirements: Multi-step checkout / onboarding form.
Key decisions:
1. Controlled vs uncontrolled: controlled for validation, uncontrolled for perf-heavy forms
2. Validation: on blur (not on every keystroke) — better UX
3. Optimistic submit: disable button after first click
4. Field-level error state, not just form-level
5. Preserve state between steps (React Context or Zustand)
Performance Patterns
| Problem | Solution |
|---|---|
| Too many DOM nodes | Virtualization (react-window, react-virtual) |
| Expensive re-renders | React.memo + useMemo + useCallback |
| Large bundle | Code splitting: React.lazy + Suspense + route-based splitting |
| Slow images | Lazy load with IntersectionObserver or loading="lazy" |
| Duplicate API calls | Cache in React Query / SWR or manual Map |
| Rapid user input | Debounce (search), Throttle (scroll/resize) |
| Cumulative layout shift | Skeleton screens, fixed dimensions on images |
State Management Decision Tree
Is state UI-only? (modal open, tab active)
└─ Yes → useState or useReducer
Is state needed by 2-3 nearby components?
└─ Yes → lift state up
Is state needed across the whole app?
└─ Yes → React Context (simple) or Zustand/Redux (complex)
Is state from a server (async)?
└─ Yes → React Query / TanStack Query — handles cache, loading, error, refetch
Related
- [[FrontEnd/React/React]] — hooks, reconciliation, performance
- [[FrontEnd/Java script/JavaScript]] — event loop, closures
- [[FrontEnd/TypeScript]] — typed component props, generic hooks
- [[FrontEnd/Java script/JavaScript Function Code Examples for Interviews]] — debounce, throttle implementations