Skip to main content
Frontend system design is about thinking through a feature before you code it. It’s the same discipline as backend system design — requirements first, then architecture, then implementation — but applied to the client. This guide covers how to gather requirements, structure your thinking, and use flowcharts to communicate your design for common frontend problems.

The Frontend System Design Process

Requirements → Architecture → Data Flow → Components → Spec. Don’t skip the first step. Each step informs the next. If you skip requirements, you’ll make architecture choices that don’t fit the problem. If you skip the data flow, you’ll build components that don’t know how to talk to each other. The diagrams are useful for communication, but the real value is in the thinking — understanding why each layer exists and what it’s responsible for.

Phase 1: Gathering Requirements

Before drawing any architecture, you need to know what you’re building. Use the same discipline as in AI-Assisted Frontend Interview. Requirements aren’t a formality. They’re the inputs to your design. “How much data?” directly determines whether you need pagination, infinite scroll, or virtualization. “Who are the users?” determines whether you optimize for mobile touch targets or desktop keyboard shortcuts. Skipping this step means you’ll either over-engineer (virtualizing 50 items) or under-engineer (rendering 10,000 DOM nodes).

Requirements Checklist

AreaKey QuestionsDrives
UsersWho? Use case? Devices?UX, layout, a11y
ScaleHow much data? Real-time?Pagination, virtualization, caching
APIContract? Pagination? Auth?Data layer, error handling
PerformanceBudgets? Heavy assets? Offline?Lazy load, code split, caching
SecurityUser input? Auth?Sanitization, token handling
AccessibilityWCAG level? Keyboard? Screen reader?Semantic HTML, ARIA, focus
Why each area matters: Users drive UX decisions — an admin dashboard and a consumer app have different layouts. Scale drives performance strategy — hundreds of items can be paginated; tens of thousands need virtualization. API shape drives your data layer — if the API doesn’t support cursor-based pagination, you can’t do infinite scroll the same way. Performance, security, and a11y are often afterthoughts; making them explicit in requirements forces you to design for them from the start.

Requirements → Architecture Mapping

Requirements don’t just inform what you build — they determine how you build it. The mapping below shows how specific requirements translate into concrete technical choices. This is the bridge between “what does the user need?” and “what library or pattern do I use?” How to read this: “1000+ items” → you need virtualization because the DOM can’t handle that many nodes; react-window or TanStack Virtual only render what’s visible. “Real-time” → you need a mechanism to refresh; polling with SWR’s revalidateInterval or WebSockets for true push. “Offline” → you need a cache layer and a service worker; Workbox + IndexedDB is the standard stack. “Multi-step form” → you need a state machine because the flow has clear steps and validation gates; a simple currentStep number plus validation per step prevents invalid progression.

Phase 2: High-Level Architecture Patterns

Every frontend feature fits into a few patterns. Identifying which one you’re building saves you from reinventing the wheel — and from using the wrong wheel. A newsfeed and a todo list need different architectures. Mixing them up leads to overcomplicated state or underpowered data handling.
PatternExamplesKey Concerns
Server-driven listNewsfeed, table, dashboardPagination, caching, loading states
User-driven localTodo, canvas, formState management, persistence, validation
Hybrid (server + local)Autocomplete, searchDebounce, cache, optimistic UI
When to use which: Server-driven features (newsfeed, table, dashboard) are dominated by “fetch data, display data, handle loading/error.” Your main concerns are caching, pagination, and keeping the UI responsive during fetches. User-driven features (todo, canvas, form) are dominated by local state — the user creates and mutates data in the browser. Your main concerns are state shape, persistence, and validation. Hybrid features (autocomplete, search) combine both: you fetch from the server based on user input, but you also need local state for the input, debouncing, and often a client-side cache to avoid redundant API calls.

Example 1: Newsfeed

A newsfeed is the canonical server-driven list. The data lives on the server; the client fetches, caches, and displays it. The main design challenges are: (1) how to load more data as the user scrolls, (2) how to handle mutations (like, comment) without blocking the UI, and (3) how to keep the feed performant with potentially hundreds of posts and images.

Requirements Gathering

QuestionAnswer (assumed)Design Impact
Who are the users?Social consumersMobile-first, touch-friendly
How much data?Hundreds of posts, infinite scrollVirtualization or pagination
Real-time?Optional (new posts)Polling or WebSocket
Interactions?Like, comment, shareOptimistic UI, local state
Images?Yes, variable sizeLazy load, aspect ratio, placeholder
Key design decisions: Cursor-based pagination (e.g. ?cursor=xyz&limit=20) is preferred over offset for feeds — it avoids duplicate or missed items when new posts arrive. React Query (or SWR) handles caching and deduplication. For “like” and similar actions, optimistic updates are essential: update the UI immediately, then sync with the server; revert and show a toast if the request fails. Images should be lazy-loaded with loading="lazy" or IntersectionObserver; use aspect-ratio placeholders to avoid layout shift.

Architecture

Data Flow

Component Hierarchy

What the layers mean: The UI layer renders PostCards; each card owns its like button and comment count. The data layer is a single useFeedQuery hook that fetches from the API, caches via React Query, and exposes fetchNextPage for infinite scroll. The state layer splits into two: optimistic state for likes (immediate UI update) and scroll position for triggering the next fetch. Keeping these separate prevents scroll logic from coupling with mutation logic. Pitfalls to avoid: Don’t fetch on every scroll event — use IntersectionObserver at the bottom of the list. Don’t skip optimistic updates for likes — the UI will feel sluggish. Don’t forget to revert on API failure. Don’t render all images eagerly — lazy load with a placeholder to keep initial render fast.

Example 2: Autocomplete

Autocomplete is a hybrid: the user types (local state), and you fetch suggestions from the server. The main design challenges are: (1) debouncing to avoid an API call on every keystroke, (2) caching to avoid redundant calls for the same query, (3) handling the loading/empty/selected states cleanly, and (4) keyboard accessibility so users can navigate and select without a mouse.

Requirements Gathering

QuestionAnswer (assumed)Design Impact
Trigger?On input, after N charsDebounce 300ms
Data source?API searchCache results, dedupe
Max suggestions?10Limit API param
Keyboard nav?YesArrow keys, Enter to select
Selection behavior?Fill input, closeControlled input
Key design decisions: Debounce at 300ms — short enough to feel responsive, long enough to batch rapid typing. Require at least 2 characters before fetching to avoid noisy or empty results. Cache by query string; React Query does this by default with the query key. The state machine (Idle → Loading → Open/Empty → Selected) prevents invalid states like “dropdown open with no results and loading” — each transition is explicit.

Architecture

State Machine

Data Flow

What the flow means: User types → debounced function fires after 300ms of no input → if query length ≥ 2, hit the API (or cache). Cache hit returns immediately; cache miss fetches and stores. The dropdown renders when you have results; keyboard arrow/enter or click selects and fills the input. The state machine ensures you never show “loading” and “empty” at once, and that blur or escape always returns to Idle. Pitfalls to avoid: Don’t forget to cancel in-flight requests when the user types again — otherwise “cat” might return after “category” and overwrite. Don’t skip keyboard nav — many users rely on it. Don’t fetch on every keystroke — you’ll hammer the API and waste bandwidth.

Example 3: Todo List

A todo list is user-driven: the data is created and mutated locally. The main design challenges are: (1) keeping state simple (a single array of todos plus a filter), (2) persisting to localStorage or an API without blocking the UI, (3) deriving the filtered list without duplicating state, and (4) handling the empty state and clear-completed flow.

Requirements Gathering

QuestionAnswer (assumed)Design Impact
Persistence?localStorage or APISync layer
Filters?All / Active / CompletedDerived state
Single or multiple lists?SingleSimpler state
Due dates?NoNo date picker
Key design decisions: Store todos as an array; the filter (All/Active/Completed) is separate state. The filtered list is derived: todos.filter(t => filter === 'All' || ...) — never store the filtered list separately. Persistence goes in a useEffect that runs when todos changes: sync to localStorage or call a mutation. For API persistence, use optimistic updates — update local state first, then sync; revert on failure.

Architecture

Data Flow

Component Structure

What the layers mean: State is minimal: todos array and filter enum. The UI is a straightforward hierarchy — input at top, list in middle, footer with count and filters. Persistence is a side effect: when todos changes, write to localStorage or trigger an API mutation. The key insight is that the filtered list is derived — you never have filteredTodos as state, only as a computed value. That keeps the source of truth in one place. Pitfalls to avoid: Don’t store filtered todos as state — you’ll get out of sync when todos change. Don’t block the UI on persistence — do it in the background. Don’t forget to sanitize user input if you’re rendering it (XSS). For API sync, handle conflicts — what if the user edits on two devices?

Example 4: Canvas Page (Canva-like)

A canvas app is user-driven with complex local state. The main design challenges are: (1) representing elements as a unified data structure (shapes, text, images with different props), (2) handling drag, resize, and transform without jank, (3) separating canvas-level transform (zoom, pan) from element-level transform (position, size), and (4) making it all work with keyboard for accessibility.

Requirements Gathering

QuestionAnswer (assumed)Design Impact
Elements?Shapes, text, imagesElement type union
Interactions?Drag, resize, rotate, deleteTransform state per element
Zoom/pan?Yes, canvas-levelCanvas transform matrix
Undo/redo?OptionalCommand pattern / history stack
Persistence?OptionalSerialize elements to JSON
Key design decisions: Elements are a discriminated union: { type: 'shape', ... } | { type: 'text', ... } | { type: 'image', ... }. Each has id, x, y, width, height, and type-specific props. Canvas zoom/pan is a separate transform applied to the container — use CSS transform for GPU acceleration. For drag, track selectedId and update the element’s x,y on mousemove; use requestAnimationFrame or throttle to avoid layout thrashing. For resize, same idea but update width, height.

Architecture

Interaction Flow

Element State Shape

What the layers mean: The canvas has two transform levels: (1) canvas zoom/pan applied to the whole container, (2) each element’s own position and size. The palette is a separate component that emits “add element” on drop. State is elements: Element[] and selectedId; mutations are addElement, updateElement, deleteElement. The element shape is a union so you can add new types (e.g. video) without breaking existing code. Pitfalls to avoid: Don’t put zoom/pan on each element — it belongs on the canvas. Don’t update state on every mousemove without throttling — you’ll flood React. Use transform for positioning, not left/top, for better performance. For undo/redo, push snapshots to a history stack; don’t try to diff — it’s error-prone.

Example 5: Table with Pagination

A data table with pagination is server-driven: the client sends page, limit, sort, and filter params, and the server returns a slice of data. The main design challenges are: (1) keeping URL or state in sync with the table params so users can share or bookmark, (2) refetching when params change without manual orchestration, (3) handling loading and empty states, and (4) making sortable headers and filters accessible.

Requirements Gathering

QuestionAnswer (assumed)Design Impact
How many rows?Hundreds to thousandsPagination, not virtualization
Sortable?Yes, server-sideAPI params: sort, order
Filterable?Yes, by columnAPI params: filter
Row actions?View, edit, deleteAction menu
Page size?10, 25, 50URL or state
Key design decisions: All table params (page, limit, sort, order, filters) live in one place — either React state or URL search params. URL is better for shareability: ?page=2&sort=date&order=desc. React Query’s queryKey includes these params, so changing them automatically triggers a refetch. The API contract should return { items: [], total: number } so you can compute total pages. Sort and filter are server-side — the client sends params, the server does the work.

Architecture

Data Flow

Component Hierarchy

What the flow means: Params (page, limit, sort, order, filters) are the single source of truth. When the user changes page, sort, or filter, you update params; React Query refetches because the query key changed. If you sync to URL, useSearchParams reads and writes — back/forward and bookmarking work. The table just renders what it receives; it doesn’t manage pagination logic. Pitfalls to avoid: Don’t fetch on mount and then filter client-side for large datasets — the server should do the work. Don’t forget to disable the “Next” button when you’re on the last page. Don’t lose filter state when changing pages — params should accumulate. For sortable headers, indicate sort direction (arrow icon) and make them keyboard-activatable.

Example 6: Registration Form

A multi-step registration form is user-driven with validation gates. The main design challenges are: (1) managing step progression so users can’t skip ahead without valid data, (2) validating each step before allowing “Next”, (3) handling back navigation without losing data, (4) submitting only when all steps are valid, and (5) accessible focus management when transitioning between steps.

Requirements Gathering

QuestionAnswer (assumed)Design Impact
Single or multi-step?Multi-step (email → password → profile)Wizard state
Validation?Per step, before nextZod/Yup schema per step
API?POST per step or single submitAffects flow
OAuth?Optional, step 0Social buttons
Key design decisions: Use a state machine: currentStep (1, 2, or 3) plus formData (accumulated across steps). Validate on “Next” — if invalid, show errors and stay; if valid, advance. “Back” never validates — you’re allowed to go back and edit. On final submit, validate the full object (Zod/Yup schema) before sending. Focus management: when moving to the next step, focus the first input of that step so keyboard users aren’t lost.

Architecture

State Machine

Data Flow

Component Structure

What the flow means: Each step has its own form fields and validation. formData accumulates — step 1 adds email, step 2 adds password, step 3 adds name. Validation runs when the user clicks “Next” or “Submit”; errors attach to fields and prevent progression. The state machine ensures you can’t get to Step 3 without valid email and password. On submit, you POST the full formData; the server may validate again (never trust the client alone). Pitfalls to avoid: Don’t validate on every keystroke — validate on blur or on “Next”. Don’t lose data when going back — formData persists. Don’t forget to handle API errors (duplicate email, network failure) — show them inline or as a toast. For a11y, use aria-live for error announcements and ensure the focus trap moves correctly between steps.

Summary: Design Before You Code

StepAction
1. GatherAsk about users, scale, API, performance, security, a11y
2. ClassifyServer-driven, user-driven, or hybrid?
3. ArchitectData flow, state shape, component hierarchy
4. DiagramFlowcharts for data flow, state machines for flows
5. SpecWrite requirements and acceptance criteria before implementation
The flowcharts in this guide are starting points. Adapt them to your specific requirements. The discipline of drawing before coding is what matters — not the exact diagram. Use the explanations to understand why each piece exists; the diagrams show what the pieces are and how they connect.