DNAπŸ—οΈ System DesignFrontend System Design Framework
πŸ‘‘ApexSystem DesignArchitectureInterview

Frontend System Design Framework

The definitive framework for frontend system design interviews. Eight structured phases that turn a vague prompt into an architecture an interviewer remembers.

Frontend System Design Framework

System design interviews expose a truth about engineers: most can build features, few can design systems. The difference is structure. This document gives you an eight-phase framework that turns "Design a social media feed" into a methodical architecture walkthrough that sounds like a staff engineer presenting an RFC.

The Eight Phases

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Phase 1 β”‚ Requirements Gathering          (5 min)          β”‚
β”‚  Phase 2 β”‚ High-Level Architecture          (5 min)         β”‚
β”‚  Phase 3 β”‚ Data Model & State Management    (7 min)         β”‚
β”‚  Phase 4 β”‚ API Design                       (5 min)         β”‚
β”‚  Phase 5 β”‚ Core Component Design            (8 min)         β”‚
β”‚  Phase 6 β”‚ Performance & Optimization       (5 min)         β”‚
β”‚  Phase 7 β”‚ Accessibility & i18n             (3 min)         β”‚
β”‚  Phase 8 β”‚ Error Handling & Resilience      (3 min)         β”‚
β”‚          β”‚ Buffer / Deep Dive               (4 min)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                        Total: ~45 min

Phase 1: Requirements Gathering

Never start drawing boxes. Start by asking questions. This signals to the interviewer that you think before you build β€” the single most valued trait in senior engineers.

Functional Requirements

What does the user do? List concrete user stories:

  • "User can scroll an infinite feed of posts"
  • "User can like, comment, and share posts"
  • "User sees real-time updates from followed accounts"

Non-Functional Requirements

These reveal architectural taste:

DimensionQuestions to Ask
ScaleHow many concurrent users? How many items in the feed?
PerformanceTarget LCP? Interaction latency budget?
OfflineDoes it need to work offline? What's cached?
PlatformDesktop only? Mobile responsive? Native webview?
A11yWCAG level? Screen reader priority?
i18nRTL support? How many locales?
SEOIs this content indexable?

Constraints & Assumptions

State them explicitly: "I'll assume we have a REST API and can add a BFF layer. I'll assume the team is 5 frontend engineers and we can't rewrite the backend."

This phase alone separates senior from mid-level. Mid-level engineers jump to components. Senior engineers define the problem space.

Phase 2: High-Level Architecture

Now draw. Start with the 30,000-foot view β€” the boxes and arrows.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          CLIENT                                  β”‚
β”‚                                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Shell   β”‚   β”‚  Router   β”‚   β”‚  Auth     β”‚   β”‚  Error      β”‚  β”‚
β”‚  β”‚  Layout  β”‚   β”‚  (pages)  β”‚   β”‚  Context  β”‚   β”‚  Boundary   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚       β”‚              β”‚               β”‚                 β”‚         β”‚
β”‚  β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                    Feature Modules                         β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚  β”‚
β”‚  β”‚  β”‚ Feed β”‚  β”‚ Profile  β”‚  β”‚ Search  β”‚  β”‚ Notifications β”‚   β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”¬β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚        β”‚           β”‚            β”‚                β”‚               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                 Shared Services Layer                       β”‚  β”‚
β”‚  β”‚  API Client β”‚ Cache β”‚ Auth β”‚ Analytics β”‚ Feature Flags      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚    BFF / API Gateway  β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β–Ό                 β–Ό                  β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚ Feed API β”‚    β”‚ User API   β”‚    β”‚ Notif API    β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What the Interviewer Is Looking For

  1. Separation of concerns β€” Shell/layout vs features vs shared services
  2. Data flow direction β€” Unidirectional, predictable
  3. API abstraction β€” A BFF or gateway pattern, not direct service calls
  4. Cross-cutting concerns β€” Auth, error handling, analytics aren't afterthoughts

Phase 3: Data Model & State Management

Define what data exists, where it lives, and how it flows.

State Topology Decision

Local State ──→ Lifted State ──→ Global State ──→ Server Cache
  (useState)     (parent prop)    (Zustand/Redux)   (React Query)
 
Rule: keep state as close to where it's used as possible.
Lift only when siblings need it. Globalize only when distant
subtrees share it. Server cache is not "state" β€” it's a cache.

Data Model Example: Feed

interface Post {
  id: string;
  author: User;
  content: PostContent;
  metrics: {
    likes: number;
    comments: number;
    shares: number;
  };
  viewerContext: {
    hasLiked: boolean;
    hasBookmarked: boolean;
  };
  createdAt: string;
}
 
interface FeedState {
  posts: Map<string, Post>;       // normalized by ID
  feedOrder: string[];             // ordered list of post IDs
  cursor: string | null;           // pagination cursor
  hasMore: boolean;
}
 
interface UIState {
  activeComposer: string | null;   // which post's comment box is open
  scrollPosition: number;
  isRefreshing: boolean;
}

Why Normalize?

Denormalized data duplicates entities. When a user updates their avatar, you'd have to find and update every post that embeds that user. Normalized data stores entities once and references by ID:

posts: { "p1": { authorId: "u1", ... } }
users: { "u1": { name: "Alice", avatar: "..." } }

One update to users["u1"] propagates everywhere.

Phase 4: API Design

REST vs GraphQL Decision Matrix

FactorRESTGraphQL
OverfetchingCommon β€” fixed response shapesSolved β€” client specifies fields
UnderfetchingMultiple round tripsSingle query, nested resolution
CachingHTTP caching works nativelyRequires normalized cache (Apollo)
Learning curveLowerHigher
Backend team controlHighShared schema ownership
File uploadsNative multipartNeeds workaround

BFF Pattern

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  React  │──────▢│    BFF      │──────▢│ Microservicesβ”‚
β”‚  App    │◀──────│  (Node.js)  │◀──────│             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The BFF aggregates, transforms, and shapes backend responses into exactly what the frontend needs. Benefits:

  • Reduces client complexity β€” No orchestrating 5 API calls per page
  • Improves performance β€” Server-to-server calls are faster than client-to-server
  • Decouples frontend from backend evolution β€” Backend can change without breaking the client

API Contract Example

// GET /api/feed?cursor=abc123&limit=20
interface FeedResponse {
  posts: Post[];
  nextCursor: string | null;
  hasMore: boolean;
}
 
// POST /api/posts/:id/like
interface LikeResponse {
  liked: boolean;
  likeCount: number;
}

Phase 5: Core Component Design

This is where you show you can think in components. Pick the 2-3 hardest components and design their API.

Feed Component Tree

<FeedPage>
  <FeedHeader filters={filters} onFilterChange={...} />
  <VirtualizedList
    items={posts}
    estimateSize={estimatePostHeight}
    overscan={5}
    renderItem={(post) => (
      <PostCard
        post={post}
        onLike={handleLike}
        onComment={handleComment}
      >
        <PostHeader author={post.author} timestamp={post.createdAt} />
        <PostContent content={post.content} />
        <PostMedia media={post.media} />
        <PostActions metrics={post.metrics} viewerContext={post.viewerContext} />
      </PostCard>
    )}
    onEndReached={loadMore}
  />
  <ScrollToTop visible={showScrollTop} />
</FeedPage>

Design Decisions to Articulate

  1. Virtualized list β€” Only render visible posts. With 10,000 posts, DOM nodes stay ~30.
  2. Composition over configuration β€” PostCard accepts children, not a variant prop with 15 options.
  3. Callback props β€” Actions bubble up. The feed page owns the mutations, not individual cards.
  4. Estimated sizing β€” Virtualization requires height estimation. Text-heavy posts vary in height.

Phase 6: Performance & Optimization

The Performance Pyramid

                    β–²
                   β•± β•²
                  β•± UX β•²            Perceived performance
                 ╱───────╲          (skeleton screens, optimistic updates)
                β•± Runtime  β•²        Virtualization, memoization, 
               ╱─────────────╲      code splitting
              β•±   Network     β•²     Caching, prefetching, compression,
             ╱─────────────────╲    CDN, image optimization
            β•±    Bundle Size    β•²   Tree shaking, lazy loading,
           ╱─────────────────────╲  dynamic imports
          β•±      Architecture     β•² SSR/SSG, streaming, edge rendering
         ╱─────────────────────────╲

Concrete Strategies for a Feed

StrategyTechniqueImpact
Virtualization@tanstack/virtualDOM nodes: 10,000 β†’ ~30
Image optimizationsrcset, WebP/AVIF, lazy loadingLCP, bandwidth
Code splittingRoute-based React.lazy()Initial bundle -40%
PrefetchingPrefetch next page on scroll near endEliminates loading states
Optimistic updatesUpdate UI before server confirmsPerceived latency β†’ 0ms
MemoizationReact.memo on PostCard, useMemo on derived dataFewer re-renders

Image Loading Strategy

function PostImage({ src, alt, width, height }: PostImageProps) {
  return (
    <picture>
      <source srcSet={`${src}?format=avif`} type="image/avif" />
      <source srcSet={`${src}?format=webp`} type="image/webp" />
      <img
        src={src}
        alt={alt}
        width={width}
        height={height}
        loading="lazy"
        decoding="async"
      />
    </picture>
  );
}

Phase 7: Accessibility & i18n

Never skip this phase. It signals maturity.

Accessibility Checklist for Feed

  • Keyboard navigation: Tab through posts, Enter to expand, Escape to close
  • Screen reader: Each post is an <article> with aria-label summarizing content
  • Focus management: After liking, focus stays on the like button (not reset to top)
  • Reduced motion: Respect prefers-reduced-motion for animations
  • Color contrast: All text meets WCAG AA (4.5:1 for body, 3:1 for large text)

i18n Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           i18n Architecture              β”‚
β”‚                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚ Locale   │──▢│ Message Catalog  β”‚    β”‚
β”‚  β”‚ Detector β”‚   β”‚ (lazy loaded)    β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                         β”‚               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚  IntlProvider (React Context)  β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β”‚                     β”‚                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚  useTranslation() / <Trans />  β”‚     β”‚
β”‚  β”‚  useIntl() for dates, numbers  β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key decisions: ICU MessageFormat for plurals, lazy-load locale bundles per route, RTL via dir="rtl" and CSS logical properties (margin-inline-start not margin-left).

Phase 8: Error Handling & Resilience

Error Boundary Architecture

<AppErrorBoundary fallback={<FullPageError />}>
  <Layout>
    <RouteErrorBoundary fallback={<RouteError />}>
      <FeedPage>
        <PostErrorBoundary fallback={<PostFallback />}>
          <PostCard post={post} />
        </PostErrorBoundary>
      </FeedPage>
    </RouteErrorBoundary>
  </Layout>
</AppErrorBoundary>

Granularity principle: A single broken post should not crash the entire feed. Wrap repeating items in their own error boundary.

Resilience Patterns

PatternDescriptionExample
Retry with backoffRetry failed requests with exponential delay1s β†’ 2s β†’ 4s β†’ give up
Graceful degradationShow stale data when network failsDisplay cached feed with "offline" banner
Circuit breakerStop calling a failing service temporarilyAfter 5 failures, skip for 30s
TimeoutDon't wait foreverAPI calls abort after 10s
Fallback UIShow something useful when a component failsSkeleton β†’ error card with retry

Network Error Handling

async function fetchWithResilience<T>(url: string, options?: RequestInit): Promise<T> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 10_000);
 
  try {
    const response = await fetch(url, { ...options, signal: controller.signal });
    if (!response.ok) throw new HttpError(response.status, response.statusText);
    return await response.json();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'AbortError') {
      throw new TimeoutError(url);
    }
    throw error;
  } finally {
    clearTimeout(timeout);
  }
}

Time Management: The 45-Minute Blueprint

 0        5       10       17       22       30       35  38  41   45
 │────────│────────│────────│────────│────────│────────│───│───│────│
 β”‚ Reqs   β”‚ HLA    β”‚ Data   β”‚ API    β”‚ Comps  β”‚ Perf   β”‚A11β”‚Errβ”‚ QA β”‚
 β”‚ Gather β”‚        β”‚ Model  β”‚ Design β”‚ Design β”‚  Opt   β”‚y  β”‚ orβ”‚    β”‚

Pro tips:

  • Set a mental checkpoint at 20 minutes β€” you should be past API design
  • If an interviewer goes deep on one phase, compress later phases rather than skipping them
  • Always mention accessibility and error handling, even if briefly β€” omitting them is a red flag
  • End with 2-3 minutes of "what I'd add with more time" β€” monitoring, feature flags, A/B testing

Example Walkthrough: "Design a Feed/Timeline"

1. Requirements (stated aloud)

"A feed showing posts from followed users. Infinite scroll, like/comment actions, real-time new post indicator. Performance target: LCP < 2.5s, smooth 60fps scroll. Desktop and mobile responsive."

2. Architecture Decision

"I'll use a BFF to aggregate user and post data into a single feed endpoint. The client will use React Query for server state and local useState for UI state. Virtualized rendering for the post list."

3. Key Trade-off Articulated

"I'm choosing cursor-based pagination over offset because users could see duplicate posts with offset pagination when new posts are inserted. The cursor is an opaque token representing the last seen post's timestamp + ID."

4. Deep Dive Chosen

"Let me go deep on the optimistic like interaction. When a user taps like, I immediately update the local cache β€” increment the count, flip the boolean. The mutation fires in the background. If it fails, I roll back the optimistic update and show a toast. This gives perceived zero-latency interaction."

This walkthrough alone β€” requirements, architecture, trade-off, deep dive β€” demonstrates the structured thinking interviewers are evaluating.

The Meta-Skill: Narrate Your Thinking

The framework isn't just about what you design β€” it's about how you communicate. Say things like:

  • "The trade-off here is..."
  • "I'm choosing X over Y because..."
  • "A constraint worth noting is..."
  • "If we had more time, I'd add..."
  • "This decision is reversible / irreversible, so..."

The framework is your skeleton. Your reasoning fills the flesh. Master both.