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 minPhase 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:
| Dimension | Questions to Ask |
|---|---|
| Scale | How many concurrent users? How many items in the feed? |
| Performance | Target LCP? Interaction latency budget? |
| Offline | Does it need to work offline? What's cached? |
| Platform | Desktop only? Mobile responsive? Native webview? |
| A11y | WCAG level? Screen reader priority? |
| i18n | RTL support? How many locales? |
| SEO | Is 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
- Separation of concerns β Shell/layout vs features vs shared services
- Data flow direction β Unidirectional, predictable
- API abstraction β A BFF or gateway pattern, not direct service calls
- 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
| Factor | REST | GraphQL |
|---|---|---|
| Overfetching | Common β fixed response shapes | Solved β client specifies fields |
| Underfetching | Multiple round trips | Single query, nested resolution |
| Caching | HTTP caching works natively | Requires normalized cache (Apollo) |
| Learning curve | Lower | Higher |
| Backend team control | High | Shared schema ownership |
| File uploads | Native multipart | Needs 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
- Virtualized list β Only render visible posts. With 10,000 posts, DOM nodes stay ~30.
- Composition over configuration β
PostCardaccepts children, not avariantprop with 15 options. - Callback props β Actions bubble up. The feed page owns the mutations, not individual cards.
- 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
| Strategy | Technique | Impact |
|---|---|---|
| Virtualization | @tanstack/virtual | DOM nodes: 10,000 β ~30 |
| Image optimization | srcset, WebP/AVIF, lazy loading | LCP, bandwidth |
| Code splitting | Route-based React.lazy() | Initial bundle -40% |
| Prefetching | Prefetch next page on scroll near end | Eliminates loading states |
| Optimistic updates | Update UI before server confirms | Perceived latency β 0ms |
| Memoization | React.memo on PostCard, useMemo on derived data | Fewer 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>witharia-labelsummarizing content - Focus management: After liking, focus stays on the like button (not reset to top)
- Reduced motion: Respect
prefers-reduced-motionfor 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
| Pattern | Description | Example |
|---|---|---|
| Retry with backoff | Retry failed requests with exponential delay | 1s β 2s β 4s β give up |
| Graceful degradation | Show stale data when network fails | Display cached feed with "offline" banner |
| Circuit breaker | Stop calling a failing service temporarily | After 5 failures, skip for 30s |
| Timeout | Don't wait forever | API calls abort after 10s |
| Fallback UI | Show something useful when a component fails | Skeleton β 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.