Intermediate to senior

Frontend Interview Prep

Fourteen chapters on HTML and CSS, core JavaScript, the event loop, browser rendering, React, state, performance, accessibility, security, TypeScript, testing, machine-coding components and frontend system design, with tested code.

Chapter 14 of 14Building and designing · Frontend System Design

Frontend System Design

A frontend system design interview asks you to architect a client application, such as a news feed, a chat app, an e-commerce listing, a collaborative editor, a video player or an autocomplete widget. It is not about backend infrastructure. It is about component architecture, data flow, state, APIs, performance, accessibility, offline behaviour and reliability on a user's device. Interviewers want a structured conversation: requirements first, then design, then deep dives and trade-offs.

1. A framework: RADIO

<!--fig:radio-->
RRequirements features non-functional needs scope AArchitecture components state data and rendering DData model entities ownership normalisation IInterface API contracts component props OOptimise performance a11y offline security Figure 1. A five-step structure for a frontend design interview.

A handy structure (named for its steps):

  1. R: Requirements. Functional (what users can do), non-functional (performance, offline, accessibility, internationalisation, browsers and devices, scale), and scope (what is out).
  2. A: Architecture. The main parts: components, client state, network layer, storage, and how they communicate. Server rendering versus client rendering.
  3. D: Data model. Entities, their shapes, the state each part owns, normalisation, caching.
  4. I: Interface (API). The endpoints and their contracts, plus the component APIs (props, events).
  5. O: Optimisations and deep dives. Performance, accessibility, error handling, offline, security, analytics, testing, rollout.

Spend a few minutes on requirements, resist diving into one component too early, and drive the conversation: state what you will cover and invite the interviewer to redirect.

2. Requirements to ask about

  • Users and devices: mobile, desktop, low-end phones, slow networks?
  • Core features and which are most important; real time or periodic updates?
  • Data scale: items per list, size of payloads, update frequency.
  • Performance targets: first load, interaction latency, scroll smoothness.
  • Offline or flaky networks?
  • SEO and shareability: server rendering needed?
  • Accessibility, internationalisation, right-to-left, theming.
  • Browser support, analytics, experimentation, feature flags.
  • Team and constraints: framework in use, backend ownership, existing design system.

3. Architecture building blocks

PieceResponsibility
View layercomponents, layout, accessibility
State layerUI state, server cache, derived data, URL state
Data layerAPI client, retries, caching, request deduplication, auth tokens, error normalisation
Real-time layerWebSocket, server-sent events or polling, reconnection and backoff
Storagememory, localStorage, IndexedDB, service worker cache
Rendering strategyCSR, SSR, SSG, streaming, islands
DeliveryCDN, code splitting, asset hashing, preloading
Cross-cuttingerror boundaries, logging and monitoring, analytics, feature flags, i18n, theming

Choosing a transport for updates

TechniqueHowUse whenCost
Pollingclient asks every secondsinfrequent updates, simplicitywasted requests, latency
Long pollingserver holds the request open until datalegacy or restricted environmentsconnection churn
Server-sent events (SSE)one-way server push over HTTPlive feeds, notificationsone direction only
WebSocketfull-duplex persistent connectionchat, collaboration, gamesstateful connections, reconnect logic, scaling on the backend

Real-time clients need reconnection with exponential backoff and jitter, resynchronisation after a gap (fetch what was missed using a last-seen ID), heartbeats, and handling of out-of-order or duplicate messages.

function backoff(attempt, base = 500, cap = 30000, rand = Math.random) {
  const exp = Math.min(cap, base * 2 ** attempt);
  return Math.floor(rand() * exp);               // "full jitter": a random wait up to the exponential ceiling
}
assert.equal(backoff(0, 500, 30000, () => 1), 500);
assert.equal(backoff(3, 500, 30000, () => 1), 4000);
assert.equal(backoff(20, 500, 30000, () => 1), 30000);       // capped
assert.equal(backoff(5, 500, 30000, () => 0), 0);

4. Data modelling on the client

  • Normalise relational data: store entities by ID in lookup tables and keep lists as arrays of IDs. Updating an entity then updates it everywhere without hunting through nested copies.
  • Separate server state from UI state.
  • Derive instead of duplicating.
  • Model request status explicitly (idle, loading, success, error) per resource.
function normalise(posts) {
  const users = {}, byId = {}, ids = [];
  for (const p of posts) {
    users[p.author.id] = p.author;
    byId[p.id] = { id: p.id, text: p.text, likes: p.likes, authorId: p.author.id };
    ids.push(p.id);
  }
  return { users, posts: byId, feed: ids };
}
const raw = [
  { id: 1, text: "a", likes: 3, author: { id: "u1", name: "Asha" } },
  { id: 2, text: "b", likes: 0, author: { id: "u1", name: "Asha" } },
];
const state = normalise(raw);
assert.deepEqual(state.feed, [1, 2]);
assert.equal(Object.keys(state.users).length, 1);            // one author record shared by both posts
state.users.u1 = { ...state.users.u1, name: "Asha K" };      // one update is visible in every post
assert.equal(state.users[state.posts[2].authorId].name, "Asha K");

5. Worked example 1: news feed

Requirements: an infinite, personalised list of posts with text and images and video; like and comment; new posts at the top; fast scroll; works on mobile.

Architecture:

  • Rendering: server-render the first page for fast first paint and SEO (if public), hydrate, then fetch more on scroll.
  • List: cursor-based pagination; an IntersectionObserver sentinel triggers the next page before the user reaches the end (a prefetch margin). Virtualise (windowing) once the list is long, keeping DOM size bounded, and handle variable heights by measuring.
  • Items: each post is a memoised component; images use srcset, fixed aspect ratios (to avoid layout shift) and lazy loading; videos autoplay muted only when visible and pause off-screen.
  • State: a normalised cache of posts and users; likes are optimistic (update at once, roll back on error); new-post arrival through SSE or polling shows a "New posts" pill rather than pushing content under the user's reading position.
  • Data: a typed API client with retries, deduplication, and cancellation on navigation.
  • Performance: code-split the comment composer and media viewers; preconnect to the image CDN; budget bundle size.
  • Accessibility: a role="feed" with article items, keyboard shortcuts with clear documentation, focus management when new content loads, alt text, reduced-motion respect.
  • Resilience: an error state per page chunk with retry, skeleton placeholders, offline caching of the last loaded page, and idempotent like requests.
  • Edge cases: deleted posts, duplicates across pages (de-duplicate by ID), clock skew, huge lists, scroll restoration on back navigation.

6. Worked example 2: autocomplete / search typeahead

Requirements: suggestions as the user types, keyboard navigation, history, fast.

  • Component API: value, onSelect, fetchSuggestions(query, signal), renderItem, minChars, debounceMs.
  • Behaviour: debounce; cancel stale requests; cache by query (and consider prefix reuse: results for "ab" can seed "abc"); minimum length; highlight matches; recent searches; handle IME composition.
  • Backend interplay: a trie or search index on the server for speed; the client should cap result count and request size; respect rate limits.
  • Accessibility: the combobox pattern with aria-activedescendant, live result counts.
  • Performance: fast response matters more than perfect ranking, so show cached or recent results instantly while fetching.
  • Failure: timeouts, offline, and empty results are visible states.
function lruCache(limit) {
  const m = new Map();
  return {
    get(k) { if (!m.has(k)) return undefined; const v = m.get(k); m.delete(k); m.set(k, v); return v; },
    set(k, v) { m.delete(k); m.set(k, v); if (m.size > limit) m.delete(m.keys().next().value); },
    get size() { return m.size; },
  };
}
const c = lruCache(2);
c.set("a", 1); c.set("b", 2); c.get("a"); c.set("c", 3);        // "b" is the least recently used
assert.equal(c.get("b"), undefined);
assert.equal(c.get("a"), 1);
assert.equal(c.size, 2);

7. Worked example 3: chat application

<!--fig:chat-->
Chat UIvirtualised list, composer Local storemessages by id, drafts Sync layeroptimistic send, retry, dedupe IndexedDB outboxsurvives refresh and offline WebSocket + RESTreal time + history, resync by last id Send: show the message at once with a client id, queue it, deliver, then reconcile with the server id and sequence number. Figure 1. Client architecture for a chat application.

Requirements: one-to-one and group chats, send and receive in real time, message history, unread counts, typing indicators, delivery and read receipts, attachments, works on flaky networks.

  • Transport: WebSocket with automatic reconnect (backoff and jitter), heartbeats, and resync on reconnect using the last message ID per conversation. Fall back to SSE or polling where sockets are blocked.
  • State: conversations list, messages per conversation (normalised, keyed by ID), connection status, drafts. Messages carry a client-generated ID so the sender can show them instantly (optimistic send) and later reconcile with the server's ID and timestamp.
  • Ordering and duplicates: sort by server sequence number, de-duplicate by ID, handle out-of-order arrival, and mark messages sending, sent, delivered, failed with retry.
  • List UI: a virtualised, bottom-anchored list; load older messages when scrolling up while preserving scroll position; "jump to latest" button; do not auto-scroll if the user has scrolled up.
  • Offline: queue outgoing messages in IndexedDB and flush on reconnect; cache recent history.
  • Unread and notifications: per-conversation counts, tab title badge, web push through a service worker, and cross-tab coordination with BroadcastChannel so that only one tab holds the connection.
  • Security: escape or sanitise message content, validate links, scan attachments server-side, and consider end-to-end encryption where required (client-side key management is a large topic).
  • Accessibility: a live region for incoming messages (polite), keyboard-reachable actions, readable timestamps.
// merge incoming messages: dedupe by id, then order by server sequence
function mergeMessages(existing, incoming) {
  const byId = new Map(existing.map(m => [m.id, m]));
  for (const m of incoming) byId.set(m.id, { ...byId.get(m.id), ...m });     // later data wins; pending state is overwritten
  return [...byId.values()].sort((a, b) => a.seq - b.seq);
}
const local = [{ id: "c1", seq: 3, text: "hi", status: "sending" }];
const merged = mergeMessages(local, [
  { id: "m0", seq: 1, text: "welcome" },
  { id: "c1", seq: 2, text: "hi", status: "sent" },               // the server confirmation of our own message
  { id: "m2", seq: 3, text: "there" },
]);
assert.deepEqual(merged.map(m => m.id), ["m0", "c1", "m2"]);
assert.equal(merged.find(m => m.id === "c1").status, "sent");
assert.equal(merged.length, 3);
  • List and filters in the URL, so the page is shareable and the back button works.
  • Server-render the listing for SEO; hydrate filters.
  • Thumbnails through an image CDN with responsive sizes and modern formats; blur-up placeholders; fixed dimensions to prevent layout shift.
  • Facets and sorting: debounce, keep the previous results visible while loading (do not flash empty), show counts.
  • Pagination or infinite scroll: prefer pagination for SEO and focus management; "load more" as a compromise.
  • Cart: optimistic add, persistence, price and stock validation at checkout (never trust cached prices).
  • Performance budgets for LCP and CLS; prefetch the product page on hover or in view.
  • Analytics and experimentation: impression tracking with IntersectionObserver, batching events with sendBeacon.

9. Cross-cutting topics to raise

  • Performance: code splitting, lazy loading, prefetching, caching headers, image optimisation, virtualisation, avoiding long tasks, Web Vitals monitoring.
  • Accessibility and internationalisation: keyboard, screen readers, text expansion, RTL, locale-aware formatting.
  • Offline and resilience: service workers, retry with backoff, optimistic updates with rollback, graceful degradation.
  • Security: XSS, CSRF, token handling, CSP, third-party scripts.
  • Observability: error tracking, performance (RUM), analytics events, session replay with privacy care.
  • Rollout: feature flags, A/B tests, gradual rollout, kill switches.
  • Design system and component API design: consistent tokens, composable components, documented props and events.
  • Micro-frontends: independent teams and deployments through module federation or iframes; trade-offs in consistency, shared dependencies and performance. Mention it as an option, not a default.
  • Monorepo, build and CI: shared packages, type checks, tests, bundle-size gates.

10. Component API design

Good component APIs are small, composable and hard to misuse.

  • Prefer composition (<Tabs><Tab/><Panel/></Tabs>) over a single component with a dozen boolean props.
  • Controlled and uncontrolled modes (value and onChange versus defaultValue).
  • Sensible defaults, accessible by default, and escape hatches (className, ref, rest props).
  • Events as callbacks with minimal, stable payloads.
  • Headless patterns: separate behaviour (state, keyboard, ARIA) from styling so teams can skin it.
  • Document and test the contract.

11. How to handle trade-off questions

State the options, say what each optimises, choose one for the stated requirements, and name when you would switch.

QuestionTypical reasoning
SSR or CSR?SSR for SEO and first paint on public content; CSR for authenticated, highly interactive apps; hybrid and streaming for both
WebSocket or SSE or polling?polling for simplicity at low frequency; SSE for one-way pushes; WebSocket for two-way, low-latency interaction
Normalised or nested state?normalised when entities appear in many places and update often
Pagination or infinite scroll?pagination for navigation, SEO and focus; infinite for browsing feeds, with virtualisation
Optimistic or confirmed updates?optimistic for low-risk, high-frequency actions with rollback; confirmed for payments and irreversible actions
Monolith or micro-frontends?monolith until team and deploy independence justify the overhead

12. Common mistakes

  • Diving into one component without establishing requirements.
  • Designing the backend instead of the client.
  • Ignoring performance, accessibility and failure states.
  • No data model: vague "state management" with no shapes or ownership.
  • Naming technologies without reasons.
  • Over-engineering (micro-frontends and offline sync for a simple form).
  • Forgetting the user's device: low memory, slow network, small screen.
  • Not discussing testing, monitoring or rollout.

13. Practice questions

  1. Design a news feed with infinite scroll, likes and live updates.
  2. Design a chat application's frontend with reconnection and offline sending.
  3. Design an autocomplete component with a clean API.
  4. Design a collaborative document editor's frontend (presence, conflict handling, undo).
  5. Design a video streaming page (player controls, adaptive bitrate, buffering UI).
  6. Design a dashboard with many live-updating charts without freezing the page.
  7. Design an image-heavy e-commerce listing page with filters.
  8. Design a reusable design-system component library for several teams.
Header Logo