Back to Notes

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

ProblemSolution
Too many DOM nodesVirtualization (react-window, react-virtual)
Expensive re-rendersReact.memo + useMemo + useCallback
Large bundleCode splitting: React.lazy + Suspense + route-based splitting
Slow imagesLazy load with IntersectionObserver or loading="lazy"
Duplicate API callsCache in React Query / SWR or manual Map
Rapid user inputDebounce (search), Throttle (scroll/resize)
Cumulative layout shiftSkeleton 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