System Design ⚑ Recently updatedUpdated on Jun 30, 2026 β€” this content may still be evolving.

Frontend System Design

How to discuss React/SPA architecture in interviews β€” component structure, state, performance, and common UI patterns at scale.


Component Architecture

The Problem

As apps grow, a single App.tsx with 2,000 lines becomes impossible to maintain, test, or onboard new developers to.

How to Structure (Student-Realistic)

Feature-based folders (recommended for projects and interviews):

src/
β”œβ”€β”€ components/          # Shared UI primitives
β”‚   └── ui/
β”‚       β”œβ”€β”€ Button.tsx
β”‚       β”œβ”€β”€ Modal.tsx
β”‚       └── Input.tsx
β”œβ”€β”€ features/
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ LoginForm.tsx
β”‚   β”‚   β”œβ”€β”€ useAuth.ts
β”‚   β”‚   └── authApi.ts
β”‚   β”œβ”€β”€ feed/
β”‚   β”‚   β”œβ”€β”€ FeedList.tsx
β”‚   β”‚   β”œβ”€β”€ FeedItem.tsx
β”‚   β”‚   β”œβ”€β”€ useFeed.ts
β”‚   β”‚   └── feedApi.ts
β”‚   └── profile/
β”‚       β”œβ”€β”€ ProfileHeader.tsx
β”‚       └── EditProfileForm.tsx
β”œβ”€β”€ hooks/               # Cross-feature hooks
β”œβ”€β”€ lib/                 # Utilities, API client
β”œβ”€β”€ pages/ or app/       # Route-level components
└── types/

Design Principles to Mention

PrincipleWhat it meansExample
Single responsibilityOne component, one jobFeedItem renders one post; doesn’t fetch the whole feed
CompositionBuild complex UI from small piecesCard + CardHeader + CardBody
Container vs presentationalLogic separate from UIFeedList (container) fetches; FeedItem (presentational) just renders props
ColocationKeep related code togetherHook + API + components in same feature folder

Atomic Design (Know the Terms)

  • Atoms: Button, Input, Label
  • Molecules: SearchBar (Input + Button)
  • Organisms: Header (Logo + Nav + SearchBar)
  • Templates/Pages: Full page layout

You don’t need to name-drop every level β€” just show you think in layers.

Interview line: β€œI’d organize by feature β€” features/feed/ owns everything feed-related. Shared primitives like Button live in components/ui/ so we don’t duplicate styles.”


State Management

The Golden Rule

Don’t put everything in global state. Most state is local. Start simple; add complexity only when needed.

State Categories

TypeWhere it livesExample
UI stateComponent useStateModal open/closed, form input values
Server stateReact Query / SWRPosts from API, user profile
Global client stateContext / Zustand / ReduxAuth user, theme, shopping cart
URL stateReact Router search paramsFilters, pagination page, search query

Decision Tree (Say This in Interviews)

Is it server data?
  YES β†’ React Query (cache, refetch, loading states built-in)
  NO β†’ Is it needed by many unrelated components?
    YES β†’ Context (light) or Zustand (heavier global state)
    NO β†’ useState in the component (or lift one level up)

React Query Example (Server State)

// hooks/useFeed.js
function useFeed(page) {
  return useQuery({
    queryKey: ['feed', page],
    queryFn: () => fetch(`/api/feed?page=${page}`).then(r => r.json()),
    staleTime: 60_000, // treat as fresh for 1 minute
  });
}

// FeedList.jsx
function FeedList() {
  const [page, setPage] = useState(1);
  const { data, isLoading, error } = useFeed(page);
  // ...
}

Why React Query over Redux for API data:

  • Automatic caching and deduplication
  • Background refetching
  • Loading/error states without boilerplate
  • No stale data sync bugs between Redux and server

When Redux Still Makes Sense

  • Complex client-side state with many interdependencies (e.g., a visual page builder)
  • Time-travel debugging requirements
  • Large team with established Redux patterns

For most student projects: React Query + Context for auth + local state is the right answer.

Prop Drilling vs Context

Prop drilling: Passing props through 5 layers of components that don’t use them.

Fix: Context for truly global data (theme, auth). Don’t Context everything β€” it causes unnecessary re-renders.

Interview line: β€œAPI data goes in React Query. Auth user in Context. Form state stays local. I’d avoid Redux unless we have complex client-only state like a multi-step wizard with cross-step validation.”


Performance

What Interviewers Want to Hear

Not micro-optimizations β€” architectural choices that keep the app fast.

Code Splitting (Route-Level)

// React.lazy β€” each route loads its own JS bundle
const FeedPage = lazy(() => import('./features/feed/FeedPage'));
const ProfilePage = lazy(() => import('./features/profile/ProfilePage'));

<Suspense fallback={<Spinner />}>
  <Routes>
    <Route path="/feed" element={<FeedPage />} />
    <Route path="/profile" element={<ProfilePage />} />
  </Routes>
</Suspense>

Impact: User visiting /login doesn’t download the entire app’s JavaScript.

List Virtualization

Rendering 10,000 DOM nodes kills performance. Virtualization only renders visible rows.

// react-window β€” renders ~20 visible items, not 10,000
import { FixedSizeList } from 'react-window';

<FixedSizeList height={600} itemCount={10000} itemSize={80}>
  {({ index, style }) => <FeedItem post={posts[index]} style={style} />}
</FixedSizeList>

Image Optimization

  • Use loading="lazy" on below-fold images
  • Serve WebP/AVIF with fallbacks
  • Correct width/height to prevent layout shift (CLS)
  • Thumbnail URLs for list views; full size on detail page

Memoization (Use Sparingly)

// Re-render only when post.id changes
const FeedItem = memo(function FeedItem({ post }) { ... });

// Expensive computation cached
const sortedPosts = useMemo(() => posts.sort(byDate), [posts]);

Don’t memo everything β€” measure first. Premature memo adds complexity without benefit.

Core Web Vitals (Know the Names)

MetricWhat it measuresTarget
LCPLargest Contentful Paint (loading)< 2.5s
INPInteraction to Next Paint (responsiveness)< 200ms
CLSCumulative Layout Shift (visual stability)< 0.1

Interview line: β€œI’d lazy-load routes, virtualize long lists, and use React Query’s staleTime to avoid refetching on every navigation. I’d measure with Lighthouse before optimizing.”


Infinite Scroll

Requirements to Clarify

  • Load more when user scrolls near bottom?
  • β€œNew items available” banner at top (Twitter-style)?
  • How many items per page?
  • What if user scrolls fast β€” debounce load requests?

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     GET /posts?cursor=abc&limit=20     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Browser   β”‚ ──────────────────────────────────────►│     API     β”‚
β”‚             │◄────────────────────────────────────── β”‚             β”‚
β”‚ FeedList    β”‚     { items: [...], nextCursor: "xyz" }β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ + sentinel  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementation Pattern (React Query Infinite)

function useInfiniteFeed() {
  return useInfiniteQuery({
    queryKey: ['feed'],
    queryFn: ({ pageParam }) =>
      fetch(`/api/feed?cursor=${pageParam || ''}&limit=20`).then(r => r.json()),
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  });
}

function FeedList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteFeed();
  const sentinelRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting && hasNextPage && !isFetchingNextPage) {
        fetchNextPage();
      }
    });
    if (sentinelRef.current) observer.observe(sentinelRef.current);
    return () => observer.disconnect();
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);

  const posts = data?.pages.flatMap(p => p.items) ?? [];

  return (
    <>
      {posts.map(post => <FeedItem key={post.id} post={post} />)}
      <div ref={sentinelRef}>{isFetchingNextPage && <Spinner />}</div>
    </>
  );
}

API Design for Infinite Scroll

Use cursor-based pagination, not offset:

GET /api/feed?cursor=eyJpZCI6MTIzfQ&limit=20

Response:
{
  "items": [...],
  "nextCursor": "eyJpZCI6MTQzfQ"
}

Why not offset? OFFSET 10000 scans 10,000 rows β€” slow and unstable if new items are inserted.

Tradeoffs to Mention

ApproachProsCons
Infinite scrollSeamless UX for feedsHard to reach footer; bad for SEO pages
Pagination buttonsClear position, shareable URLsMore clicks
Virtualized infinite scrollBest of both for huge listsMore complex

Requirements to Clarify

  • Search products? Users? Addresses?
  • How many results to show (5–10)?
  • Debounce delay (300ms typical)?
  • Minimum characters before search (2–3)?
  • Keyboard navigation (arrow keys, Enter)?

Architecture

User types "lap" ──► debounce 300ms ──► GET /api/search?q=lap&limit=8
                                              β”‚
                                              β–Ό
                                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                        β”‚ Search    β”‚
                                        β”‚ Index     β”‚
                                        β”‚ (SQL FTS  β”‚
                                        β”‚  or ES)   β”‚
                                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Frontend Implementation

function useAutocomplete(query) {
  const debouncedQuery = useDebounce(query, 300);

  return useQuery({
    queryKey: ['search', debouncedQuery],
    queryFn: () =>
      fetch(`/api/search?q=${encodeURIComponent(debouncedQuery)}&limit=8`)
        .then(r => r.json()),
    enabled: debouncedQuery.length >= 2, // don't search on 1 character
  });
}

Accessibility (Interviewers Notice)

  • role="combobox" on input
  • role="listbox" on dropdown
  • aria-activedescendant for keyboard highlight
  • Announce result count to screen readers

Backend at Scale

ScaleApproach
< 10K rowsSQL LIKE or PostgreSQL full-text search
10K – 1M rowsDedicated index (Elasticsearch, Typesense, Algolia)
Any scaleDebounce + rate limit on client and server

Search index sync: On product create/update β†’ async job updates search index (eventual consistency OK for search).

Caching Autocomplete

  • Cache popular prefixes in Redis: search:lap β†’ top 8 results
  • Short TTL (30–60 seconds) β€” search data changes often
  • CDN not useful (dynamic, per-query)

Interview line: β€œI’d debounce input 300ms, cancel in-flight requests with AbortController, and use cursor pagination on the full results page. For the index, I’d start with Postgres full-text and move to Elasticsearch if we exceed 100K products.”


Frontend System Design Checklist

Before ending any frontend design answer:

  • Component/folder structure explained
  • State management choice justified (local vs server vs global)
  • Performance: code splitting, virtualization, image strategy
  • API contract for lists (cursor pagination)
  • Loading, error, and empty states mentioned
  • Accessibility for interactive patterns

Next: design-a-social-media-app.md or backend-system-design.md