Questions
Designing Pinterest's Frontend Architecture
A comprehensive guide to building a scalable and responsive frontend for a visual discovery platform like Pinterest.
Designing Pinterest's Frontend Architecture
Learn how to build a scalable, responsive frontend for a visual discovery platform with infinite scrolling, masonry layouts, and performance optimization.
Problem Statement
Design a frontend architecture for a visual discovery platform similar to Pinterest with the following capabilities:
- Masonry layout for displaying images of varying sizes
- Infinite scrolling for seamless content discovery
- Image optimization and lazy loading
- Real-time search and filtering
- Responsive design for all devices
- Performance optimization for large datasets
- User interactions (save, like, share)
- Personalized recommendations
Clarifying Questions
Functional Requirements
- Content Scale: How many images/pins should the platform handle?
- User Base: How many concurrent users need to be supported?
- Image Types: What formats and sizes should be supported?
- Search Capabilities: Should search be real-time or indexed?
- Personalization: How sophisticated should recommendations be?
- Social Features: What level of social interaction is needed?
Technical Constraints
- Performance: What's the acceptable load time for images?
- Mobile Support: Should it work on low-end mobile devices?
- Browser Support: Which browsers and versions need support?
- Network Conditions: How should it handle slow connections?
- SEO Requirements: What level of SEO optimization is needed?
High-Level Architecture
1. Component Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Feed View │ │ State Manager │ │ Image Cache │
│ (Main App) │◄──►│ (Redux/Zustand)│◄──►│ (Service Worker)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Masonry Grid │ │ API Client │ │ CDN/Storage │
│ (Virtualized) │ │ (REST/GraphQL)│ │ (Cloudinary) │
└─────────────────┘ └─────────────────┘ └─────────────────┘2. Core Components
Feed Component
- Main container for the infinite scroll feed
- Intersection Observer for scroll detection
- Loading states and error handling
- Search and filter integration
Masonry Grid Component
- Dynamic layout calculation for varying image sizes
- Virtual scrolling for performance
- Responsive breakpoints for different screen sizes
- Smooth animations and transitions
Pin Component
- Image optimization with multiple sizes
- Lazy loading with placeholder
- User interactions (save, like, share)
- Hover effects and overlays
Search Component
- Real-time search with debouncing
- Search suggestions and autocomplete
- Filter options (category, color, size)
- Search history and trending topics
Masonry Layout Implementation
Core Masonry Logic
interface Pin {
id: string;
title: string;
description?: string;
imageUrl: string;
width: number;
height: number;
aspectRatio: number;
author: {
id: string;
name: string;
avatar: string;
};
likes: number;
saves: number;
tags: string[];
createdAt: Date;
}
class MasonryLayout {
private columns: number;
private columnHeights: number[];
private containerWidth: number;
private gutter: number;
constructor(containerWidth: number, columns: number = 4, gutter: number = 16) {
this.containerWidth = containerWidth;
this.columns = columns;
this.gutter = gutter;
this.columnHeights = new Array(columns).fill(0);
}
calculateLayout(pins: Pin[]): PinLayout[] {
const columnWidth = (this.containerWidth - (this.gutter * (this.columns - 1))) / this.columns;
return pins.map(pin => {
// Find the shortest column
const shortestColumnIndex = this.columnHeights.indexOf(Math.min(...this.columnHeights));
// Calculate position
const x = shortestColumnIndex * (columnWidth + this.gutter);
const y = this.columnHeights[shortestColumnIndex];
// Calculate scaled dimensions
const scaledHeight = columnWidth / pin.aspectRatio;
const scaledWidth = columnWidth;
// Update column height
this.columnHeights[shortestColumnIndex] += scaledHeight + this.gutter;
return {
pin,
x,
y,
width: scaledWidth,
height: scaledHeight,
columnIndex: shortestColumnIndex,
};
});
}
updateContainerWidth(width: number) {
this.containerWidth = width;
this.columnHeights = new Array(this.columns).fill(0);
}
}
interface PinLayout {
pin: Pin;
x: number;
y: number;
width: number;
height: number;
columnIndex: number;
}React Implementation
const MasonryGrid: React.FC<{ pins: Pin[]; columns?: number }> = ({ pins, columns = 4 }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [layout, setLayout] = useState<PinLayout[]>([]);
const [containerWidth, setContainerWidth] = useState(0);
useEffect(() => {
const updateLayout = () => {
if (containerRef.current) {
const width = containerRef.current.offsetWidth;
setContainerWidth(width);
const masonry = new MasonryLayout(width, columns);
const newLayout = masonry.calculateLayout(pins);
setLayout(newLayout);
}
};
updateLayout();
window.addEventListener('resize', updateLayout);
return () => window.removeEventListener('resize', updateLayout);
}, [pins, columns]);
return (
<div ref={containerRef} className="masonry-container">
<div
className="masonry-grid"
style={{
position: 'relative',
height: Math.max(...layout.map(item => item.y + item.height)) + 16,
}}
>
{layout.map((item) => (
<PinCard
key={item.pin.id}
pin={item.pin}
style={{
position: 'absolute',
left: item.x,
top: item.y,
width: item.width,
height: item.height,
}}
/>
))}
</div>
</div>
);
};Infinite Scrolling
Intersection Observer Implementation
const useInfiniteScroll = (
onLoadMore: () => Promise<void>,
hasMore: boolean,
loading: boolean
) => {
const observerRef = useRef<IntersectionObserver | null>(null);
const loadingRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (loading) return;
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore) {
onLoadMore();
}
},
{ threshold: 0.1 }
);
observerRef.current = observer;
if (loadingRef.current) {
observer.observe(loadingRef.current);
}
return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [onLoadMore, hasMore, loading]);
return loadingRef;
};
const PinterestFeed: React.FC = () => {
const [pins, setPins] = useState<Pin[]>([]);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const loadMorePins = useCallback(async () => {
if (loading || !hasMore) return;
setLoading(true);
try {
const newPins = await fetchPins(page, 20);
setPins(prev => [...prev, ...newPins]);
setPage(prev => prev + 1);
setHasMore(newPins.length === 20);
} catch (error) {
console.error('Failed to load pins:', error);
} finally {
setLoading(false);
}
}, [page, loading, hasMore]);
const loadingRef = useInfiniteScroll(loadMorePins, hasMore, loading);
return (
<div className="pinterest-feed">
<MasonryGrid pins={pins} />
{loading && (
<div ref={loadingRef} className="loading-indicator">
<Spinner />
<p>Loading more pins...</p>
</div>
)}
{!hasMore && pins.length > 0 && (
<div className="end-of-feed">
<p>You've reached the end!</p>
</div>
)}
</div>
);
};Image Optimization
Responsive Images with Multiple Sizes
interface ImageSizes {
thumbnail: string; // 150x150
small: string; // 300x300
medium: string; // 600x600
large: string; // 1200x1200
original: string; // Original size
}
const generateImageUrls = (originalUrl: string): ImageSizes => {
const baseUrl = originalUrl.split('?')[0];
return {
thumbnail: `${baseUrl}?w=150&h=150&fit=crop&crop=center`,
small: `${baseUrl}?w=300&h=300&fit=crop&crop=center`,
medium: `${baseUrl}?w=600&h=600&fit=crop&crop=center`,
large: `${baseUrl}?w=1200&h=1200&fit=crop&crop=center`,
original: originalUrl,
};
};
const PinCard: React.FC<{ pin: Pin; style?: CSSProperties }> = ({ pin, style }) => {
const [imageLoaded, setImageLoaded] = useState(false);
const [currentImage, setCurrentImage] = useState<string>('');
const imageRef = useRef<HTMLImageElement>(null);
const imageSizes = generateImageUrls(pin.imageUrl);
useEffect(() => {
const loadOptimalImage = () => {
const containerWidth = imageRef.current?.parentElement?.offsetWidth || 300;
let optimalSize: keyof ImageSizes;
if (containerWidth <= 200) optimalSize = 'thumbnail';
else if (containerWidth <= 400) optimalSize = 'small';
else if (containerWidth <= 800) optimalSize = 'medium';
else optimalSize = 'large';
setCurrentImage(imageSizes[optimalSize]);
};
loadOptimalImage();
window.addEventListener('resize', loadOptimalImage);
return () => window.removeEventListener('resize', loadOptimalImage);
}, [pin.imageUrl]);
return (
<div className="pin-card" style={style}>
<div className="pin-image-container">
{!imageLoaded && (
<div className="image-placeholder">
<Skeleton width="100%" height="200px" />
</div>
)}
<img
ref={imageRef}
src={currentImage}
alt={pin.title}
className={`pin-image ${imageLoaded ? 'loaded' : ''}`}
onLoad={() => setImageLoaded(true)}
loading="lazy"
/>
<div className="pin-overlay">
<div className="pin-actions">
<button className="save-button">
Save
</button>
<button className="share-button">
Share
</button>
</div>
</div>
</div>
<div className="pin-content">
<h3 className="pin-title">{pin.title}</h3>
{pin.description && (
<p className="pin-description">{pin.description}</p>
)}
<div className="pin-meta">
<div className="pin-author">
<img src={pin.author.avatar} alt={pin.author.name} />
<span>{pin.author.name}</span>
</div>
<div className="pin-stats">
<span>{pin.likes} likes</span>
<span>{pin.saves} saves</span>
</div>
</div>
</div>
</div>
);
};Search and Filtering
Real-time Search Implementation
const useSearch = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Pin[]>([]);
const [loading, setLoading] = useState(false);
const [suggestions, setSuggestions] = useState<string[]>([]);
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery.trim()) {
searchPins(debouncedQuery);
getSuggestions(debouncedQuery);
} else {
setResults([]);
setSuggestions([]);
}
}, [debouncedQuery]);
const searchPins = async (searchQuery: string) => {
setLoading(true);
try {
const searchResults = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}`);
const data = await searchResults.json();
setResults(data.pins);
} catch (error) {
console.error('Search failed:', error);
} finally {
setLoading(false);
}
};
const getSuggestions = async (searchQuery: string) => {
try {
const response = await fetch(`/api/suggestions?q=${encodeURIComponent(searchQuery)}`);
const data = await response.json();
setSuggestions(data.suggestions);
} catch (error) {
console.error('Failed to get suggestions:', error);
}
};
return {
query,
setQuery,
results,
loading,
suggestions,
};
};
const SearchComponent: React.FC = () => {
const { query, setQuery, results, loading, suggestions } = useSearch();
const [showSuggestions, setShowSuggestions] = useState(false);
return (
<div className="search-container">
<div className="search-input-wrapper">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setShowSuggestions(true)}
placeholder="Search for ideas..."
className="search-input"
/>
{loading && <Spinner className="search-spinner" />}
</div>
{showSuggestions && suggestions.length > 0 && (
<div className="search-suggestions">
{suggestions.map((suggestion, index) => (
<div
key={index}
className="suggestion-item"
onClick={() => {
setQuery(suggestion);
setShowSuggestions(false);
}}
>
{suggestion}
</div>
))}
</div>
)}
{results.length > 0 && (
<div className="search-results">
<h3>Search Results</h3>
<MasonryGrid pins={results} />
</div>
)}
</div>
);
};Performance Optimization
Virtual Scrolling for Large Lists
import { FixedSizeList as List } from 'react-window';
const VirtualizedMasonry: React.FC<{ pins: Pin[] }> = ({ pins }) => {
const [layout, setLayout] = useState<PinLayout[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (containerRef.current) {
const width = containerRef.current.offsetWidth;
const masonry = new MasonryLayout(width, 4);
const newLayout = masonry.calculateLayout(pins);
setLayout(newLayout);
}
}, [pins]);
const getItemHeight = (index: number) => {
const item = layout[index];
return item ? item.height + 16 : 200; // Default height + margin
};
const Row = ({ index, style }: { index: number; style: CSSProperties }) => {
const item = layout[index];
if (!item) return null;
return (
<div style={style}>
<PinCard
pin={item.pin}
style={{
width: item.width,
height: item.height,
}}
/>
</div>
);
};
return (
<div ref={containerRef} className="virtualized-masonry">
<List
height={800}
itemCount={layout.length}
itemSize={getItemHeight}
width="100%"
>
{Row}
</List>
</div>
);
};Service Worker for Caching
// service-worker.js
const CACHE_NAME = 'pinterest-v1';
const IMAGE_CACHE = 'pinterest-images-v1';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/static/js/bundle.js',
'/static/css/main.css',
]);
})
);
});
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
// Cache API responses
event.respondWith(
fetch(event.request)
.then((response) => {
const clonedResponse = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clonedResponse);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
} else if (event.request.destination === 'image') {
// Cache images
event.respondWith(
caches.open(IMAGE_CACHE).then((cache) => {
return cache.match(event.request).then((response) => {
if (response) {
return response;
}
return fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
}
});Responsive Design
CSS Grid Implementation
.masonry-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 16px;
}
.masonry-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
grid-auto-rows: 0;
grid-auto-flow: dense;
}
.pin-card {
break-inside: avoid;
margin-bottom: 16px;
border-radius: 16px;
overflow: hidden;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.pin-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.pin-image-container {
position: relative;
width: 100%;
overflow: hidden;
}
.pin-image {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s ease;
}
.pin-card:hover .pin-image {
transform: scale(1.05);
}
.pin-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.pin-card:hover .pin-overlay {
opacity: 1;
}
/* Responsive breakpoints */
@media (max-width: 768px) {
.masonry-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 8px;
}
.pin-card {
margin-bottom: 8px;
border-radius: 12px;
}
}
@media (max-width: 480px) {
.masonry-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 4px;
}
}Best Practices
Performance
- Lazy loading for images with intersection observer
- Image optimization with multiple sizes and formats
- Virtual scrolling for large lists
- Service worker caching for offline support
User Experience
- Smooth animations and transitions
- Responsive design for all screen sizes
- Progressive loading with skeleton screens
- Error handling with fallback images
SEO and Accessibility
- Semantic HTML structure
- Alt text for all images
- Keyboard navigation support
- Screen reader compatibility
Mobile Optimization
- Touch-friendly interactions
- Optimized images for mobile networks
- Reduced animations on low-end devices
- Offline-first approach
Interview Tips
What Interviewers Look For:
- Masonry layout implementation understanding
- Performance optimization techniques
- Responsive design considerations
- Image handling and optimization
Sample Questions:
- "How would you handle 100,000 images efficiently?"
- "What if images fail to load?"
- "How would you implement real-time search?"
- "How would you optimize for mobile devices?"
Key Points to Mention:
- Use CSS Grid or Flexbox for responsive layouts
- Implement lazy loading for performance
- Optimize images with multiple sizes
- Consider mobile-first design approach
- Plan for offline scenarios with caching
Next: WYSIWYG Editor Design - Learn how to build a rich text editor with real-time collaboration.