Master frontend system design interviews for JavaScript roles. Learn how to design scalable web applications, component architectures, state management systems, and real-time features with practical examples.
What is Frontend System Design?
Frontend system design interviews test your ability to architect complex client-side applications. Unlike backend system design (which focuses on databases, servers, and scaling), frontend design focuses on component architecture, state management, rendering performance, network optimization, and user experience.
These questions appear in senior/staff-level JavaScript interviews at companies like Google, Meta, Amazon, and top startups. Even mid-level roles increasingly include a "design a feature" round.
Requirements
- Display posts with text, images, and engagement counts
- Infinite scroll — load more posts as user scrolls down
- Like/comment actions with optimistic UI
- Real-time updates for new posts (without requiring refresh)
Architecture
// Component hierarchy
const FeedArchitecture = {
App: {
Header: { NotificationBell: {}, SearchBar: {} },
Feed: {
VirtualizedList: {
PostCard: {
PostHeader: {}, // Author info, timestamp
PostContent: {}, // Text, media
PostActions: {}, // Like, comment, share buttons
CommentSection: {} // Lazy-loaded on expand
}
},
InfiniteScrollTrigger: {}, // IntersectionObserver
LoadingSkeleton: {}
}
}
};
State Management
class FeedStore {
constructor() {
this.posts = []; // Ordered array of post objects
this.cursor = null; // Pagination cursor for next page
this.loading = false;
this.hasMore = true;
}
async loadMore() {
if (this.loading || !this.hasMore) return;
this.loading = true;
this.notify(); // Re-render loading state
try {
const response = await fetch(
`/api/feed?cursor=${this.cursor}&limit=20`
);
const { posts, nextCursor } = await response.json();
this.posts = [...this.posts, ...posts];
this.cursor = nextCursor;
this.hasMore = nextCursor !== null;
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
this.notify();
}
}
// Optimistic like — update UI immediately, revert on failure
async likePost(postId) {
const post = this.posts.find(p => p.id === postId);
const previousState = { liked: post.liked, count: post.likeCount };
// Optimistic update
post.liked = !post.liked;
post.likeCount += post.liked ? 1 : -1;
this.notify();
try {
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
} catch (error) {
// Revert on failure
post.liked = previousState.liked;
post.likeCount = previousState.count;
this.notify();
}
}
}
class InfiniteScroll {
constructor(container, loadMore) {
this.loadMore = loadMore;
// Create sentinel element at the bottom of the list
this.sentinel = document.createElement('div');
this.sentinel.className = 'scroll-sentinel';
container.appendChild(this.sentinel);
this.observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
this.loadMore();
}
},
{
root: null, // viewport
rootMargin: '200px', // trigger 200px before reaching bottom
threshold: 0
}
);
this.observer.observe(this.sentinel);
}
destroy() {
this.observer.disconnect();
this.sentinel.remove();
}
}
// 1. Virtualized rendering — only render visible posts
class VirtualizedList {
constructor({ itemHeight, overscan = 5, container }) {
this.itemHeight = itemHeight;
this.overscan = overscan;
this.container = container;
this.container.addEventListener('scroll',
this.throttledUpdate.bind(this)
);
}
getVisibleRange() {
const scrollTop = this.container.scrollTop;
const viewportHeight = this.container.clientHeight;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.ceil(
(scrollTop + viewportHeight) / this.itemHeight
);
return {
start: Math.max(0, startIndex - this.overscan),
end: endIndex + this.overscan
};
}
throttledUpdate = throttle(() => this.render(), 16); // ~60fps
}
// 2. Image lazy loading
function lazyLoadImages(container) {
const images = container.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
});
images.forEach(img => observer.observe(img));
}
// 3. Skeleton screens for perceived performance
function renderSkeleton(count) {
return Array.from({ length: count }, () => `
<div class="post-skeleton">
<div class="skeleton-avatar pulse"></div>
<div class="skeleton-text pulse"></div>
<div class="skeleton-image pulse"></div>
</div>
`).join('');
}
Design Example 2: Autocomplete Search Component
Requirements
- Debounced search with API calls
- Keyboard navigation (arrow keys, Enter, Escape)
- Highlight matching text in results
- Cache recent searches
- Accessible (ARIA roles, screen reader support)
Implementation
class Autocomplete {
constructor({ input, endpoint, minChars = 2, debounceMs = 300 }) {
this.input = input;
this.endpoint = endpoint;
this.minChars = minChars;
this.results = [];
this.activeIndex = -1;
this.cache = new Map();
this.abortController = null;
// Debounced search
this.debouncedSearch = this.debounce(
this.search.bind(this), debounceMs
);
this.setupDOM();
this.setupEventListeners();
}
setupDOM() {
// Create results dropdown
this.dropdown = document.createElement('ul');
this.dropdown.role = 'listbox';
this.dropdown.id = 'autocomplete-results';
this.dropdown.className = 'autocomplete-dropdown hidden';
this.input.parentNode.appendChild(this.dropdown);
// ARIA attributes for accessibility
this.input.setAttribute('role', 'combobox');
this.input.setAttribute('aria-autocomplete', 'list');
this.input.setAttribute('aria-controls', 'autocomplete-results');
this.input.setAttribute('aria-expanded', 'false');
}
setupEventListeners() {
this.input.addEventListener('input', (e) => {
const query = e.target.value.trim();
if (query.length >= this.minChars) {
this.debouncedSearch(query);
} else {
this.hideDropdown();
}
});
this.input.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.moveActive(1);
break;
case 'ArrowUp':
e.preventDefault();
this.moveActive(-1);
break;
case 'Enter':
e.preventDefault();
this.selectActive();
break;
case 'Escape':
this.hideDropdown();
break;
}
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.input.contains(e.target) &&
!this.dropdown.contains(e.target)) {
this.hideDropdown();
}
});
}
async search(query) {
// Check cache first
if (this.cache.has(query)) {
this.renderResults(this.cache.get(query), query);
return;
}
// Cancel previous request
if (this.abortController) {
this.abortController.abort();
}
this.abortController = new AbortController();
try {
const response = await fetch(
`${this.endpoint}?q=${encodeURIComponent(query)}`,
{ signal: this.abortController.signal }
);
const data = await response.json();
// Cache the results
this.cache.set(query, data.results);
// Evict old cache entries (keep last 50)
if (this.cache.size > 50) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.renderResults(data.results, query);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Search failed:', error);
}
}
}
renderResults(results, query) {
this.results = results;
this.activeIndex = -1;
if (results.length === 0) {
this.dropdown.innerHTML = `
<li class="no-results">No results found</li>
`;
} else {
this.dropdown.innerHTML = results.map((item, index) => `
<li role="option"
id="result-${index}"
class="autocomplete-item"
data-index="${index}">
${this.highlightMatch(item.label, query)}
</li>
`).join('');
}
this.showDropdown();
}
highlightMatch(text, query) {
const regex = new RegExp(`(${this.escapeRegex(query)})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
moveActive(direction) {
const newIndex = this.activeIndex + direction;
if (newIndex >= -1 && newIndex < this.results.length) {
this.activeIndex = newIndex;
this.updateActiveHighlight();
}
}
updateActiveHighlight() {
const items = this.dropdown.querySelectorAll('.autocomplete-item');
items.forEach((item, i) => {
item.classList.toggle('active', i === this.activeIndex);
});
// Update ARIA
if (this.activeIndex >= 0) {
this.input.setAttribute(
'aria-activedescendant', `result-${this.activeIndex}`
);
} else {
this.input.removeAttribute('aria-activedescendant');
}
}
selectActive() {
if (this.activeIndex >= 0 && this.activeIndex < this.results.length) {
const selected = this.results[this.activeIndex];
this.input.value = selected.label;
this.hideDropdown();
this.onSelect?.(selected);
}
}
showDropdown() {
this.dropdown.classList.remove('hidden');
this.input.setAttribute('aria-expanded', 'true');
}
hideDropdown() {
this.dropdown.classList.add('hidden');
this.input.setAttribute('aria-expanded', 'false');
this.activeIndex = -1;
}
debounce(fn, ms) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), ms);
};
}
}
Design Example 3: Real-Time Collaborative Document
Key Design Decisions
// Architecture decisions to discuss with interviewer:
const designDecisions = {
transport: {
choice: 'WebSocket',
why: 'Bidirectional, low-latency push from server',
fallback: 'Server-Sent Events for read-only updates'
},
conflictResolution: {
choice: 'Operational Transformation (OT) or CRDT',
why: 'Multiple users editing same content simultaneously',
tradeoff: 'OT is simpler but requires central server; CRDT is P2P but complex'
},
rendering: {
choice: 'ContentEditable with custom command handling',
why: 'Native text editing behavior with programmatic control',
tradeoff: 'Cross-browser inconsistencies need careful handling'
},
offlineSupport: {
choice: 'Operation queue with IndexedDB persistence',
why: 'Users can edit offline, sync when reconnected',
implementation: 'Queue local operations, replay on reconnect'
}
};
// Simplified operation queue for offline support
class OperationQueue {
constructor() {
this.queue = [];
this.synced = true;
}
addOperation(op) {
this.queue.push({
...op,
timestamp: Date.now(),
clientId: this.clientId
});
if (this.synced) {
this.flush();
}
}
async flush() {
while (this.queue.length > 0) {
const op = this.queue[0];
try {
await this.sendToServer(op);
this.queue.shift(); // Remove on success
} catch (error) {
this.synced = false;
// Will retry when connection restores
break;
}
}
}
onReconnect() {
this.synced = true;
this.flush();
}
}
Common Frontend System Design Topics
| Topic | Key Concerns |
|---|
| News Feed | Infinite scroll, virtualization, optimistic updates |
| Chat Application | WebSocket, message ordering, presence, typing indicators |
| E-commerce Cart | State sync, inventory validation, price calculation |
| Image Gallery | Lazy loading, virtualization, responsive images |
| Form Builder | Dynamic components, validation, schema-driven rendering |
| Dashboard | Charting, data polling, widget layout system |
| Video Player | Streaming, buffering, adaptive bitrate, custom controls |
| Notification System | Permission API, Service Workers, priority queue |
// 1. RequestAnimationFrame for smooth animations
function animateScroll(targetY, duration = 300) {
const startY = window.scrollY;
const startTime = performance.now();
function step(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3); // Ease-out cubic
window.scrollTo(0, startY + (targetY - startY) * eased);
if (progress < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
// 2. Web Workers for heavy computation
const worker = new Worker('compute.js');
worker.postMessage({ type: 'PROCESS', data: largeDataset });
worker.onmessage = (e) => {
renderResults(e.data); // UI stays responsive
};
// 3. Service Worker for caching strategy
// (Stale-while-revalidate pattern)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
const fetched = fetch(event.request).then(response => {
const clone = response.clone();
caches.open('v1').then(cache => cache.put(event.request, clone));
return response;
});
return cached || fetched;
})
);
});
Summary
Frontend system design interviews test your ability to think architecturally about client-side applications. The key skills are: breaking complex features into component hierarchies, choosing appropriate state management patterns, optimizing rendering performance, handling network edge cases (offline, slow connections, race conditions), and communicating tradeoffs clearly. Practice by designing real applications out loud — pick any popular web app and explain how you would build it from scratch using vanilla JavaScript or your framework of choice.