A comprehensive guide to frontend system design interviews using the RADIO framework: Requirements, Architecture, Data Model, Interface Definition, Optimizations
- News Feed (e.g. Facebook)
- Autocomplete
- Rich Text Editor
- Google Docs
- Video Streaming (e.g. Netflix)
- E-commerce Marketplace (e.g. Amazon)
- Travel Booking (e.g. Airbnb)
- Chat App (e.g. Messenger)
- Photo Sharing (e.g. Instagram)
- Email Client (e.g. Microsoft Outlook)
Core Features:
- Display infinite scrolling feed of posts
- Create new posts (text, images, videos)
- Interact with posts (like, comment, share)
- Real-time updates for new posts
Functional Requirements:
- Users can view posts from friends/followed accounts
- Users can create text posts with optional media
- Users can like, comment, and share posts
- Feed updates in real-time when new posts arrive
Non-Functional Requirements:
- Performance: Initial load < 2s, smooth 60fps scrolling
- Support desktop and mobile web
- Offline: Show cached posts, queue actions
- Accessibility: Screen reader support, keyboard navigation
Out of Scope:
- Stories, Groups, Marketplace
- Video calls, Gaming
- Ad serving system
┌─────────────────────────────────────────┐
│ Server (API) │
│ - Feed endpoints │
│ - Post creation endpoints │
│ - WebSocket for real-time updates │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Controller │
│ - Manages data flow │
│ - Handles API calls │
│ - WebSocket connection │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Client Store │
│ - Feed posts cache │
│ - User data │
│ - Pagination state │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Feed UI │
│ ┌─────────────────────────────────┐ │
│ │ Post Composer │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Feed Post (repeated) │ │
│ │ - Author info │ │
│ │ - Content │ │
│ │ - Interaction buttons │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | Post | id, created_time, content, media_url, media_type, author (User), reactions (object), comments_count, shares_count |
| Server | User | id, name, profile_photo_url, username |
| Server | Feed | posts (Post[]), next_cursor, has_more |
| Server | Reaction | type (like/love/haha/wow/sad/angry), count, user_reacted (boolean) |
| Client | NewPost | content, media_file, is_uploading, upload_progress |
| Client | UIState | is_loading, error, active_post_id |
Server APIs:
// GET /feed - Fetch feed posts
GET /api/feed?cursor={cursor}&size={size}
Response: {
posts: Post[],
pagination: {
next_cursor: string,
has_more: boolean
}
}
// POST /posts - Create new post
POST /api/posts
Body: {
content: string,
media?: File,
media_type?: 'image' | 'video'
}
Response: {
post: Post
}
// POST /posts/:id/reactions - React to post
POST /api/posts/:id/reactions
Body: {
type: 'like' | 'love' | 'haha' | 'wow' | 'sad' | 'angry'
}
// WebSocket events
WS /feed/updates
Events:
- new_post: { post: Post }
- post_updated: { post_id: string, changes: Partial<Post> }Component APIs:
// Feed Component
interface FeedProps {
userId: string;
onPostCreated?: (post: Post) => void;
}
// Post Component
interface PostProps {
post: Post;
onReact: (type: ReactionType) => Promise<void>;
onComment: () => void;
onShare: () => void;
}
// Post Composer Component
interface PostComposerProps {
onSubmit: (content: string, media?: File) => Promise<void>;
placeholder?: string;
}Performance:
- Virtual Scrolling: Render only visible posts (react-window, react-virtualized)
- Image Lazy Loading: Load images as they enter viewport (Intersection Observer)
- Code Splitting: Lazy load video player, comment section
- Debounced Reactions: Batch reaction updates
- Optimistic UI: Immediately show reactions/comments before server confirms
Network:
- Pagination: Cursor-based pagination (better than offset for real-time feeds)
- Prefetching: Load next page when user is 80% through current page
- Request Deduplication: Cache identical API calls
- WebSocket: Real-time updates without polling
- CDN: Serve media files from CDN with appropriate cache headers
User Experience:
- Skeleton Screens: Show loading placeholders
- Pull to Refresh: Mobile gesture support
- Infinite Scroll: Automatic loading
- Error Recovery: Retry failed requests, show error boundaries
- Offline Support: IndexedDB cache, queue actions when offline
Accessibility:
- ARIA labels for all interactive elements
- Keyboard navigation (Tab, Enter, Space)
- Screen reader announcements for new posts
- Focus management for modals
- Proper heading hierarchy
Security:
- XSS Prevention: Sanitize user content
- CSRF Protection: Token-based
- Content Security Policy headers
- Rate limiting on client
Core Features:
- Product listing and search
- Product detail page
- Shopping cart
- Checkout flow
- Order history
Functional Requirements:
- Users can browse and search products
- Users can add/remove items from cart
- Users can complete purchase
- Real-time inventory updates
- Support for multiple payment methods
Non-Functional Requirements:
- Performance: Page load < 2s, cart updates instant
- Scalability: Handle traffic spikes (Black Friday)
- SEO: Server-side rendering for product pages
- Security: PCI compliance, secure checkout
- Mobile: Responsive design
┌─────────────────────────────────────────┐
│ API Server │
│ - Product catalog │
│ - Inventory service │
│ - Cart service │
│ - Order service │
│ - Payment gateway │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Controller │
│ - API calls │
│ - State management │
│ - Analytics tracking │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Client Store │
│ - Product catalog │
│ - Cart state │
│ - User data │
│ - Checkout state │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ UI Components │
│ ┌─────────────────────────────────┐ │
│ │ Product Listing │ │
│ │ - Filters │ │
│ │ - Sort options │ │
│ │ - Pagination │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Product Detail Page │ │
│ │ - Image gallery │ │
│ │ - Add to cart │ │
│ │ - Reviews │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Shopping Cart │ │
│ │ - Item list │ │
│ │ - Quantity controls │ │
│ │ - Price summary │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Checkout Flow │ │
│ │ - Shipping info │ │
│ │ - Payment method │ │
│ │ - Order review │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | Product | id, name, description, price, images, category, rating, reviews_count, in_stock, variants |
| Server | CartItem | product_id, quantity, variant, price_at_add |
| Server | Order | id, user_id, items, total, status, shipping_address, payment_method, created_at |
| Client | Cart | items (CartItem[]), subtotal, tax, shipping, total |
| Client | CheckoutState | step, shipping_info, payment_info, is_processing |
| Client | Filters | category, price_range, rating, in_stock_only |
Server APIs:
// GET /products - Search/browse products
GET /api/products?q={query}&category={cat}&page={page}&limit={limit}
Response: {
products: Product[],
total: number,
page: number,
total_pages: number
}
// GET /products/:id - Get product details
GET /api/products/:id
// POST /cart - Add to cart
POST /api/cart
Body: {
product_id: string,
quantity: number,
variant?: string
}
// GET /cart - Get current cart
GET /api/cart
Response: {
items: CartItem[],
subtotal: number,
tax: number,
shipping: number,
total: number
}
// PUT /cart/:itemId - Update cart item
PUT /api/cart/:itemId
Body: { quantity: number }
// DELETE /cart/:itemId - Remove from cart
DELETE /api/cart/:itemId
// POST /orders - Create order
POST /api/orders
Body: {
items: CartItem[],
shipping_address: Address,
payment_method: PaymentMethod,
payment_token: string
}Component APIs:
interface ProductListProps {
products: Product[];
onFilterChange: (filters: Filters) => void;
onSortChange: (sort: SortOption) => void;
onLoadMore: () => void;
}
interface CartProps {
items: CartItem[];
onUpdateQuantity: (itemId: string, quantity: number) => void;
onRemove: (itemId: string) => void;
onCheckout: () => void;
}
interface CheckoutProps {
cart: Cart;
onSubmit: (orderData: OrderData) => Promise<void>;
onBack: () => void;
}Performance:
- SSR/SSG: Server-side render product pages for SEO
- Image Optimization: WebP, lazy loading, responsive images
- Code Splitting: Load checkout code only when needed
- Optimistic Updates: Instant cart updates
- Memoization: Cache product listings, filters
- Virtual Scrolling: For long product lists
Cart Management:
// Optimistic cart updates
function addToCart(product, quantity) {
// 1. Update UI immediately
dispatch({ type: 'ADD_TO_CART', product, quantity });
// 2. Sync with server
api.addToCart(product.id, quantity)
.catch(error => {
// Rollback on failure
dispatch({ type: 'ROLLBACK_CART', product });
showError('Failed to add to cart');
});
}
// Persist cart
localStorage.setItem('cart', JSON.stringify(cartState));
// Or sync to server for logged-in usersSearch & Filtering:
- Debounced Search: 300ms delay
- Client-side Filtering: For small datasets
- URL State: Sync filters to URL for bookmarking
- Faceted Search: Show available filters with counts
Checkout:
- Progress Indicator: Show current step (1/3)
- Form Validation: Real-time validation
- Address Autocomplete: Use Google Places API
- Payment: Stripe/PayPal integration
- Guest Checkout: Don't force account creation
User Experience:
- Persistent Cart: Save across sessions
- Recently Viewed: Track product history
- Recommendations: "You may also like"
- Quick View: Modal for quick product preview
- Stock Notifications: Alert when out-of-stock item available
- One-click Checkout: Save payment/shipping info
Security:
- HTTPS: All pages, especially checkout
- PCI Compliance: Never store card numbers
- CSRF Tokens: Protect state-changing operations
- Rate Limiting: Prevent abuse
- Input Sanitization: Prevent XSS
Analytics:
- Track product views, cart additions
- Conversion funnel analysis
- A/B testing for checkout flow
- Abandoned cart tracking
Core Features:
- Search listings (map + list view)
- Listing detail page
- Booking flow with calendar
- Reviews and ratings
- Host/guest messaging
Functional Requirements:
- Users can search by location, dates, guests
- Interactive map showing available listings
- Users can book with date selection
- Real-time availability updates
- Price calculation with fees
Non-Functional Requirements:
- Performance: Search results < 1s, map smooth at 60fps
- Mobile responsive (50%+ mobile traffic)
- Accessibility compliant
- Support multiple currencies/languages
┌─────────────────────────────────────────┐
│ API Server │
│ - Search service (Elasticsearch) │
│ - Listing service │
│ - Booking service │
│ - Payment service │
│ - Messaging service │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Controller │
│ - Search logic │
│ - Map interaction │
│ - Booking flow │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Client Store │
│ - Search results │
│ - Map state │
│ - Booking state │
│ - User data │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ UI Components │
│ ┌─────────────────────────────────┐ │
│ │ Search Bar │ │
│ │ - Location autocomplete │ │
│ │ - Date picker │ │
│ │ - Guest selector │ │
│ └─────────────────────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Map View │ │ List View │ │
│ │ - Pins │ │ - Cards │ │
│ │ - Clusters │ │ - Filters │ │
│ └──────────────┘ └──────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Listing Detail │ │
│ │ - Photo gallery │ │
│ │ - Amenities │ │
│ │ - Reviews │ │
│ │ - Booking widget │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | Listing | id, title, description, location (lat/lng), price_per_night, images, amenities, host_id, rating, reviews_count, max_guests |
| Server | Booking | id, listing_id, user_id, check_in, check_out, guests, total_price, status |
| Server | Review | id, listing_id, user_id, rating, comment, created_at |
| Client | SearchParams | location, check_in, check_out, guests, price_range, amenities |
| Client | MapState | center, zoom, bounds, visible_listings, selected_listing_id |
| Client | BookingDraft | selected_dates, guests, price_breakdown, payment_info |
Server APIs:
// GET /search - Search listings
GET /api/listings/search?
location={location}&
check_in={date}&
check_out={date}&
guests={num}&
ne_lat={lat}&ne_lng={lng}&
sw_lat={lat}&sw_lng={lng}
Response: {
listings: Listing[],
total: number
}
// GET /listings/:id - Get listing details
GET /api/listings/:id?check_in={date}&check_out={date}
Response: {
listing: Listing,
availability: Date[],
price_breakdown: {
base_price: number,
cleaning_fee: number,
service_fee: number,
total: number
}
}
// POST /bookings - Create booking
POST /api/bookings
Body: {
listing_id: string,
check_in: string,
check_out: string,
guests: number,
payment_method: string,
payment_token: string
}
// GET /listings/:id/reviews - Get reviews
GET /api/listings/:id/reviews?page={page}&limit={limit}Component APIs:
interface SearchBarProps {
onSearch: (params: SearchParams) => void;
initialValues?: SearchParams;
}
interface MapViewProps {
listings: Listing[];
center: LatLng;
zoom: number;
onBoundsChange: (bounds: Bounds) => void;
onListingClick: (listingId: string) => void;
selectedListingId?: string;
}
interface BookingWidgetProps {
listing: Listing;
availability: Date[];
onBook: (bookingData: BookingData) => Promise<void>;
checkIn?: Date;
checkOut?: Date;
} string;
}
interface BookingWidgetProps {
listing: Listing;
availability: Date[];
onBook: (bookingData: BookingData) => Promise<void>;
checkIn?: Date;
checkOut?: Date;
}Map Performance:
// Use map clustering for many pins
import { MarkerClusterer } from '@googlemaps/markerclusterer';
const clusterer = new MarkerClusterer({
map,
markers,
algorithm: new SuperClusterAlgorithm({ radius: 100 })
});
// Load listings only in visible bounds
map.addListener('bounds_changed', () => {
const bounds = map.getBounds();
fetchListingsInBounds(bounds);
});
// Debounce map movements
const debouncedFetch = debounce(fetchListingsInBounds, 300);Search:
- Geospatial Search: Use Elasticsearch with geo queries
- Autocomplete: Google Places API for location
- Debounced Search: 300ms delay
- URL State: Sync search params to URL
- Caching: Cache search results by parameters
Image Gallery:
- Progressive Loading: Low-res → high-res
- Lazy Loading: Load images as user scrolls
- Preload Next: Preload next image in gallery
- CDN: Serve from image CDN with transforms
Calendar:
- Blocked Dates: Show unavailable dates
- Price Calendar: Show price per night
- Minimum Stay: Enforce minimum nights
- Smart Defaults: Suggest popular date ranges
User Experience:
- Instant Search: Show results as you type
- Map Sync: Hovering card highlights map pin
- Saved Searches: Let users save search criteria
- Wishlists: Save favorite listings
- Flexible Dates: "±3 days" option
- Guest Reviews: Show both host and property reviews
Mobile Optimization:
- Touch Gestures: Pinch zoom on map
- Bottom Sheet: Listing details slide up
- Simplified Filters: Mobile-friendly filter UI
- App-like Experience: PWA with install prompt
Booking Flow:
- Progressive Disclosure: Show steps gradually
- Price Transparency: Clear breakdown of fees
- Calendar Integration: Add to Google/Apple calendar
- Confirmation: Email + SMS confirmation
- Cancellation Policy: Clear display
Core Features:
- Real-time messaging
- Message history
- Typing indicators
- Read receipts
- Media sharing (images, files)
Functional Requirements:
- Send/receive messages in real-time
- Support for 1:1 and group chats
- Messages persist across devices
- Online/offline status
- Push notifications
Non-Functional Requirements:
- Performance: Messages delivered within 100ms
- Reliability: No message loss
- Scalability: Support millions of concurrent users
- End-to-end encryption (optional)
- Works offline with sync
┌─────────────────────────────────────────┐
│ Server │
│ - WebSocket server │
│ - Message store (DB) │
│ - Presence service │
│ - Push notification service │
└─────────────────────────────────────────┘
↕ WebSocket
┌─────────────────────────────────────────┐
│ Connection Manager │
│ - WebSocket connection │
│ - Reconnection logic │
│ - Message queue │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Client Store │
│ - Conversations │
│ - Messages (indexed) │
│ - User presence │
│ - Unread counts │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ UI Components │
│ ┌─────────────────────────────────┐ │
│ │ Conversation List │ │
│ │ - Recent chats │ │
│ │ - Unread badges │ │
│ │ - Last message preview │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Message Thread │ │
│ │ - Message bubbles │ │
│ │ - Timestamps │ │
│ │ - Read receipts │ │
│ │ - Typing indicator │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Message Input │ │
│ │ - Text area │ │
│ │ - Media upload │ │
│ │ - Send button │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | Message | id, conversation_id, sender_id, content, type (text/image/file), timestamp, status (sent/delivered/read) |
| Server | Conversation | id, participants, type (direct/group), last_message, updated_at |
| Server | User | id, name, avatar, status (online/offline/away), last_seen |
| Client | LocalMessage | temp_id, is_sending, is_failed, retry_count |
| Client | TypingState | conversation_id, users_typing, last_update |
| Client | UnreadCount | conversation_id, count, last_read_message_id |
WebSocket Protocol:
// Client -> Server
{
type: 'message',
data: {
conversation_id: string,
content: string,
type: 'text' | 'image' | 'file',
temp_id: string // Client-generated ID
}
}
{
type: 'typing',
data: {
conversation_id: string,
is_typing: boolean
}
}
{
type: 'read',
data: {
conversation_id: string,
message_id: string
}
}
// Server -> Client
{
type: 'message',
data: {
id: string, // Server-generated ID
temp_id: string, // Match with client temp_id
conversation_id: string,
sender_id: string,
content: string,
timestamp: number
}
}
{
type: 'typing',
data: {
conversation_id: string,
user_id: string,
is_typing: boolean
}
}
{
type: 'presence',
data: {
user_id: string,
status: 'online' | 'offline',
last_seen: number
}
}HTTP APIs:
// GET /conversations - Get conversation list
GET /api/conversations
// GET /conversations/:id/messages - Get message history
GET /api/conversations/:id/messages?before={messageId}&limit={limit}
// POST /conversations - Create new conversation
POST /api/conversations
Body: {
participants: string[],
type: 'direct' | 'group'
}
// POST /messages/upload - Upload media
POST /api/messages/upload
Body: FormData with fileReal-time Communication:
// WebSocket with reconnection
class ChatWebSocket {
constructor() {
this.connect();
this.messageQueue = [];
}
connect() {
this.ws = new WebSocket('wss://chat.example.com');
this.ws.onopen = () => {
this.flushQueue();
};
this.ws.onclose = () => {
// Exponential backoff reconnection
setTimeout(() => this.connect(), this.getBackoffDelay());
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
}
send(message) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
this.messageQueue.push(message);
}
}
}Message Storage:
// Use IndexedDB for local message storage
const db = await openDB('chat-db', 1, {
upgrade(db) {
const messageStore = db.createObjectStore('messages', { keyPath: 'id' });
messageStore.createIndex('conversation_id', 'conversation_id');
messageStore.createIndex('timestamp', 'timestamp');
}
});
// Query messages efficiently
const messages = await db.getAllFromIndex(
'messages',
'conversation_id',
conversationId
);Performance:
- Virtual Scrolling: Render only visible messages
- Message Batching: Send multiple messages together
- Lazy Loading: Load older messages on scroll
- Image Compression: Compress before upload
- Debounced Typing: Send typing indicator every 2s max
Optimistic UI:
// Show message immediately, update when confirmed
function sendMessage(content) {
const tempId = generateTempId();
const optimisticMessage = {
temp_id: tempId,
content,
sender_id: currentUserId,
timestamp: Date.now(),
status: 'sending'
};
// Add to UI immediately
dispatch({ type: 'ADD_MESSAGE', message: optimisticMessage });
// Send to server
ws.send({ type: 'message', data: { content, temp_id: tempId } });
}
// When server confirms
ws.onmessage = (event) => {
const { id, temp_id } = event.data;
// Replace temp message with real one
dispatch({ type: 'CONFIRM_MESSAGE', temp_id, id });
};Typing Indicators:
// Debounce typing events
const sendTyping = debounce((conversationId, isTyping) => {
ws.send({
type: 'typing',
data: { conversation_id: conversationId, is_typing: isTyping }
});
}, 1000);
// Stop showing after 3 seconds of no updates
setTimeout(() => {
setTypingUsers([]);
}, 3000);no updates
setTimeout(() => {
setTypingUsers([]);
}, 3000);User Experience:
- Message Grouping: Group consecutive messages from same sender
- Timestamp Formatting: "Just now", "5m ago", "Yesterday"
- Link Previews: Show rich previews for URLs
- Emoji Picker: Quick emoji insertion
- Reply/Quote: Reference previous messages
- Search: Full-text search across conversations
- Unread Badges: Clear visual indicators
Offline Support:
- Queue Messages: Store in IndexedDB when offline
- Sync on Reconnect: Send queued messages
- Offline Indicator: Show connection status
- Message Status: Show sending/sent/delivered/read
Security:
- End-to-end Encryption: Optional Signal Protocol
- Message Expiration: Self-destructing messages
- Authentication: JWT tokens
- Rate Limiting: Prevent spam
Notifications:
- Push Notifications: Web Push API
- Badge Count: Show unread count
- Sound Alerts: Configurable sounds
- Desktop Notifications: When tab not focused
Core Features:
- Photo feed with infinite scroll
- Photo upload with filters
- Like, comment, share
- User profiles
- Stories (ephemeral content)
Functional Requirements:
- Users can upload photos/videos
- Apply filters before posting
- View feed of followed users
- Like and comment on posts
- View user profiles with grid layout
Non-Functional Requirements:
- Performance: Feed loads in < 2s
- Image quality: Balance quality vs load time
- Mobile-first design (80%+ mobile usage)
- Real-time likes/comments
- Accessibility for screen readers
┌─────────────────────────────────────────┐
│ CDN / Media Server │
│ - Original images │
│ - Thumbnails (multiple sizes) │
│ - Videos (HLS) │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ API Server │
│ - Feed service │
│ - Upload service │
│ - Social graph │
│ - WebSocket (real-time) │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Controller │
│ - Feed management │
│ - Image processing │
│ - Real-time updates │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Client Store │
│ - Feed posts │
│ - User profiles │
│ - Engagement data │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ UI Components │
│ ┌─────────────────────────────────┐ │
│ │ Feed View │ │
│ │ - Post cards (infinite) │ │
│ │ - Stories bar │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Post Card │ │
│ │ - Image/video │ │
│ │ - Like, comment, share │ │
│ │ - Caption │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Upload Flow │ │
│ │ - Image selection │ │
│ │ - Filter application │ │
│ │ - Caption editor │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Profile View │ │
│ │ - Photo grid │ │
│ │ - Stats (followers/following) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | Post | id, user_id, image_url, caption, filter, likes_count, comments_count, created_at, location |
| Server | User | id, username, profile_pic, bio, followers_count, following_count, posts_count |
| Server | Comment | id, post_id, user_id, text, created_at |
| Server | Like | id, post_id, user_id, created_at |
| Server | Story | id, user_id, media_url, created_at, expires_at, views_count |
| Client | UploadDraft | image_data, selected_filter, caption, is_uploading, upload_progress |
| Client | FeedState | posts, cursor, has_more, is_loading |
Server APIs:
// GET /feed - Get personalized feed
GET /api/feed?cursor={cursor}&limit={limit}
Response: {
posts: Post[],
next_cursor: string,
has_more: boolean
}
// POST /posts - Create new post
POST /api/posts
Body: FormData {
image: File,
caption: string,
filter: string,
location?: string
}
// POST /posts/:id/like - Like a post
POST /api/posts/:id/like
// DELETE /posts/:id/like - Unlike a post
DELETE /api/posts/:id/like
// POST /posts/:id/comments - Add comment
POST /api/posts/:id/comments
Body: { text: string }
// GET /users/:username - Get user profile
GET /api/users/:username
Response: {
user: User,
posts: Post[],
is_following: boolean
}
// GET /stories - Get stories from following
GET /api/stories
Response: {
stories: Array<{ user: User, stories: Story[] }>
}Component APIs:
interface FeedProps {
onLoadMore: () => void;
onPostClick: (postId: string) => void;
}
interface PostCardProps {
post: Post;
onLike: () => void;
onComment: () => void;
onShare: () => void;
}
interface UploadFlowProps {
onComplete: (post: Post) => void;
onCancel: () => void;
}
interface ImageFilterProps {
imageData: string;
selectedFilter: string;
onFilterChange: (filter: string) => void;
filters: Filter[];
}Image Processing:
// Client-side image compression before upload
async function compressImage(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Resize to max 1080px width
const maxWidth = 1080;
const scale = Math.min(1, maxWidth / img.width);
canvas.width = img.width * scale;
canvas.height = img.height * scale;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(resolve, 'image/jpeg', 0.85);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
// Apply filters using CSS or Canvas
const filters = {
clarendon: 'contrast(1.2) saturate(1.35)',
gingham: 'brightness(1.05) hue-rotate(-10deg)',
juno: 'contrast(1.2) saturate(1.4) brightness(1.1)',
// ... more filters
};
// Or use Canvas for complex filters
function applyCanvasFilter(imageData, filterMatrix) {
// Apply convolution matrix
// ... filter logic
}Feed Performance:
- Virtual Scrolling: Render only visible posts
- Progressive Image Loading:
- Show blur placeholder (< 1KB)
- Load thumbnail (200px)
- Load full resolution on interaction
- Lazy Load: Images load as they approach viewport
- Prefetch: Load next page before user reaches end
- Image Formats: WebP with JPEG fallback
Real-time Updates:
// WebSocket for real-time likes/comments
ws.on('post_update', ({ post_id, likes_count, comments_count }) => {
dispatch({
type: 'UPDATE_POST',
payload: { post_id, likes_count, comments_count }
});
});
// Optimistic UI for likes
function toggleLike(postId) {
const isLiked = likedPosts.has(postId);
// Update UI immediately
dispatch({
type: 'TOGGLE_LIKE',
payload: { post_id: postId, increment: isLiked ? -1 : 1 }
});
// Sync with server
api.toggleLike(postId).catch(() => {
// Revert on failure
dispatch({
type: 'TOGGLE_LIKE',
payload: { post_id: postId, increment: isLiked ? 1 : -1 }
});
});
}Upload Flow:
- Progressive Upload: Show progress bar
- Background Upload: Continue in background
- Retry Logic: Auto-retry failed uploads
- Draft Saving: Save drafts locally
- Chunked Upload: For large videos
Profile Grid:
// Responsive grid with aspect ratio
<div className="grid grid-cols-3 gap-1">
{posts.map(post => (
<div className="aspect-square">
<img src={post.thumbnail_url} className="w-full h-full object-cover" />
</div>
))}
</div>
// Lazy load profile posts
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMorePosts();
}
});
});Stories:
- Auto-advance: Progress to next story after 5s
- Tap Navigation: Left tap = previous, right tap = next
- Preload: Preload next story
- Temporary Storage: Cache for 24h, then delete
User Experience:
- Double-tap to Like: Native-app feel
- Pull to Refresh: Update feed
- Swipe Gestures: Navigate between posts (mobile)
- Haptic Feedback: On like (mobile)
- Activity Feed: Notifications for likes/comments
- Explore Page: Discover new content
Accessibility:
- Alt text for images
- Screen reader support
- Keyboard navigation
- High contrast mode
- Caption support for videos
Analytics:
- Track engagement rate
- Monitor upload success rate
- A/B test feed algorithms
- Track filter usage
Core Features:
- Email list with folders
- Read/compose emails
- Rich text editor
- Attachments
- Search functionality
Functional Requirements:
- Display inbox with unread count
- Compose new emails with formatting
- Reply/forward emails
- Organize with folders/labels
- Search across all emails
Non-Functional Requirements:
- Performance: Load inbox < 1s, handle 10k+ emails
- Reliability: No lost emails/drafts
- Offline: Read cached emails, queue sends
- Accessibility: Full keyboard navigation
- Security: Encryption, spam filtering
┌─────────────────────────────────────────┐
│ Email Server │
│ - IMAP/POP3/Exchange │
│ - SMTP (sending) │
│ - Search index │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ API Gateway │
│ - Email sync service │
│ - Search service │
│ - Attachment service │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Sync Controller │
│ - Periodic sync (IMAP IDLE) │
│ - Conflict resolution │
│ - Offline queue │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Client Store │
│ - Email cache (IndexedDB) │
│ - Folder structure │
│ - Draft emails │
│ - Settings │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ UI Components │
│ ┌──────────┐ ┌─────────────────────┐ │
│ │ Sidebar │ │ Email List │ │
│ │ - Inbox │ │ - Subject │ │
│ │ - Sent │ │ - Preview │ │
│ │ - Drafts │ │ - Unread badge │ │
│ │ - Folders│ └─────────────────────┘ │
│ └──────────┘ ┌─────────────────────┐ │
│ │ Reading Pane │ │
│ │ - Email content │ │
│ │ - Attachments │ │
│ │ - Actions │ │
│ └─────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Compose Window │ │
│ │ - To/Cc/Bcc fields │ │
│ │ - Subject │ │
│ │ - Rich text editor │ │
│ │ - Attachments │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | id, thread_id, from, to, cc, bcc, subject, body_html, body_text, date, is_read, is_starred, folder, attachments, has_attachments |
|
| Server | Folder | id, name, unread_count, total_count, parent_id |
| Server | Attachment | id, filename, size, mime_type, url |
| Client | DraftEmail | to, cc, bcc, subject, body, attachments, last_saved, is_sending |
| Client | SyncState | last_sync_time, is_syncing, sync_error, pending_actions |
| Client | UIState | selected_folder, selected_email_id, is_composing, search_query |
Server APIs:
// GET /emails - List emails in folder
GET /api/emails?folder={folder}&limit={limit}&offset={offset}
Response: {
emails: Email[],
total: number,
unread: number
}
// GET /emails/:id - Get email details
GET /api/emails/:id
Response: { email: Email }
// POST /emails/send - Send email
POST /api/emails/send
Body: {
to: string[],
cc?: string[],
bcc?: string[],
subject: string,
body: string,
attachments?: string[] // IDs from upload
}
// PUT /emails/:id - Update email (mark read, star, move)
PUT /api/emails/:id
Body: {
is_read?: boolean,
is_starred?: boolean,
folder?: string
}
// POST /search - Search emails
POST /api/search
Body: {
query: string,
folder?: string,
from?: string,
has_attachments?: boolean
}
// POST /attachments/upload - Upload attachment
POST /api/attachments/upload
Body: FormData with file
Response: { id: string, url: string }Component APIs:
interface EmailListProps {
folder: string;
onEmailSelect: (emailId: string) => void;
selectedEmailId?: string;
}
interface EmailViewerProps {
email: Email;
onReply: () => void;
onReplyAll: () => void;
onForward: () => void;
onDelete: () => void;
}
interface ComposeWindowProps {
mode: 'new' | 'reply' | 'forward';
initialData?: Partial<DraftEmail>;
onSend: (email: DraftEmail) => Promise<void>;
onSaveDraft: (email: DraftEmail) => Promise<void>;
onDiscard: () => void;
}Email Sync:
// Use IMAP IDLE for push notifications
// Or long-polling for web
async function startSync() {
while (true) {
try {
const response = await fetch('/api/sync', {
method: 'POST',
body: JSON.stringify({ last_sync: lastSyncTime })
});
const { new_emails, updated_emails, deleted_emails } = await response.json();
// Update local store
updateLocalCache(new_emails, updated_emails, deleted_emails);
lastSyncTime = Date.now();
} catch (error) {
// Exponential backoff
await sleep(getBackoffDelay());
}
}
}
// Sync on visibility change
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
syncEmails();
}
});Performance:
- Virtual Scrolling: List with 10k+ emails
- Email Pagination: Load 50 emails at a time
- Lazy Load: Email bodies loaded on selection
- IndexedDB: Store emails locally
- Service Worker: Offline support
Search:
// Client-side search for cached emails
function searchLocal(query) {
return db.emails
.where('subject').startsWithIgnoreCase(query)
.or('body').startsWithIgnoreCase(query)
.or('from').startsWithIgnoreCase(query)
.toArray();
}
// Server-side search for all emails
async function searchServer(query) {
return fetch('/api/search', {
method: 'POST',
body: JSON.stringify({ query })
});
}
// Combined search
async function search(query) {
// Show local results immediately
const localResults = await searchLocal(query);
setResults(localResults);
// Fetch server results
const serverResults = await searchServer(query);
setResults(serverResults);
}Draft Saving:
// Auto-save drafts every 30 seconds
const autoSave = debounce(async (draft) => {
try {
await saveDraft(draft);
showNotification('Draft saved');
} catch (error) {
showError('Failed to save draft');
}
}, 30000);
// Save to IndexedDB immediately
function saveDraftLocally(draft) {
db.drafts.put({
...draft,
last_saved: Date.now()
});
}Attachments:
- Progressive Upload: Show upload progress
- Chunked Upload: For large files (> 10MB)
- Drag and Drop: Easy file attachment
- Preview: Images, PDFs in browser
- Download All: Zip multiple attachments
User Experience:
- Keyboard Shortcuts:
c- Composer- Replyf- Forward/- Searchj/k- Next/previous emailEnter- Open email
- Split View: List + reading pane
- Conversation Threading: Group related emails
- Smart Compose: Auto-complete suggestions
- Undo Send: 5-second window to cancel
- Priority Inbox: AI-powered importance
Offline Support:
// Queue actions when offline
const offlineQueue = [];
function sendEmail(email) {
if (!navigator.onLine) {
offlineQueue.push({ type: 'send', data: email });
showNotification('Email will be sent when online');
return;
}
api.sendEmail(email);
}
// Process queue when back online
window.addEventListener('online', async () => {
for (const action of offlineQueue) {
await processAction(action);
}
offlineQueue.length = 0;
});Security:
- Content Security Policy: Prevent XSS
- Sanitize HTML: Clean email bodies
- Phishing Detection: Warn on suspicious links
- SPF/DKIM: Verify sender authenticity
- Encryption: TLS for transport, optional PGP
Accessibility:
- Full keyboard navigation
- Screen reader optimized
- ARIA labels for all controls
- Skip links for navigation
- High contrast mode
Core Features:
- Opens on click/hover
- Keyboard navigation
- Nested submenus
- Positioning (avoid viewport edges)
- Close on outside click
Functional Requirements:
- Toggle open/close
- Navigate with arrow keys
- Select item with Enter
- Close with Escape
- Support icons and dividers
Non-Functional Requirements:
- Accessible (ARIA)
- Smooth animations
- Mobile touch support
- RTL language support
┌─────────────────────────────────────────┐
│ Dropdown Component │
│ ┌─────────────────────────────────┐ │
│ │ Trigger Button │ │
│ │ [Menu ▼] │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Menu Panel (Portal) │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Menu Item 1 │ │ │
│ │ ├─────────────────────────┤ │ │
│ │ │ Menu Item 2 → │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ Submenu Item 1 │ │ │ │
│ │ │ │ Submenu Item 2 │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ ├─────────────────────────┤ │ │
│ │ │ Menu Item 3 │ │ │
│ │ └─────────────────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
interface MenuItem {
id: string;
label: string;
icon?: ReactNode;
onClick?: () => void;
disabled?: boolean;
submenu?: MenuItem[];
divider?: boolean; // Render as divider
}
interface DropdownState {
isOpen: boolean;
activeIndex: number;
openSubmenus: Set<string>;
triggerRect: DOMRect;
}interface DropdownProps {
// Trigger
trigger: ReactNode | ((props: { isOpen: boolean }) => ReactNode);
// Menu items
items: MenuItem[];
// Behavior
openOn?: 'click' | 'hover';
closeOnSelect?: boolean; // Default: true
// Positioning
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end' | 'left' | 'right';
offset?: [number, number]; // [x, y] offset
// Callbacks
onOpenChange?: (isOpen: boolean) => void;
onSelect?: (item: MenuItem) => void;
// Customization
className?: string;
menuClassName?: string;
}Implementation:
function Dropdown({ trigger, items, placement = 'bottom-start', ...props }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const triggerRef = useRef(null);
const menuRef = useRef(null);
// Position menu
const position = useFloating({
placement,
middleware: [offset(8), flip(), shift({ padding: 8 })]
});
// Keyboard navigation
const handleKeyDown = (e) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setActiveIndex((i) => Math.max(i - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
if (activeIndex >= 0) {
items[activeIndex].onClick?.();
setIsOpen(false);
}
break;
case 'Escape':
setIsOpen(false);
triggerRef.current?.focus();
break;
}
};
// Close on outside click
useClickOutside([triggerRef, menuRef], () => setIsOpen(false));
return (
<>
<button
ref={triggerRef}
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-haspopup="true"
>
{typeof trigger === 'function' ? trigger({ isOpen }) : trigger}
</button>
{isOpen && createPortal(
<div
ref={menuRef}
role="menu"
style={{
position: 'absolute',
top: position.y,
left: position.x
}}
onKeyDown={handleKeyDown}
>
{items.map((item, index) => (
<MenuItem
key={item.id}
item={item}
isActive={index === activeIndex}
onClick={() => {
item.onClick?.();
if (props.closeOnSelect !== false) {
setIsOpen(false);
}
}}
/>
))}
</div>,
document.body
)}
</>
);
}Positioning Library:
// Use Floating UI (Popper.js successor)
import { useFloating, offset, flip, shift } from '@floating-ui/react';
const { x, y, strategy, refs } = useFloating({
placement: 'bottom-start',
middleware: [
offset(8), // Gap between trigger and menu
flip(), // Flip if doesn't fit
shift({ padding: 8 }) // Shift to stay in viewport
]
});Accessibility:
role="menu"on containerrole="menuitem"on itemsaria-expandedon triggeraria-haspopup="true"on triggeraria-activedescendantfor active item- Focus management
- Keyboard navigation (arrows, enter, escape)
User Experience:
- Smooth Animations: Fade in/out, slide down
- Hover Intent: Delay before opening submenu (300ms)
- Touch Support: Handle touch events on mobile
- Visual Feedback: Highlight active item
- Dividers: Separate item groups
- Icons: Support for icons alongside labels
Submenu Handling:
// Open submenu on hover (desktop) or click (mobile)
function handleSubmenuTrigger(item) {
if (isMobile) {
// Click to open
setOpenSubmenus(new Set([item.id]));
} else {
// Hover with delay
hoverTimeout = setTimeout(() => {
setOpenSubmenus(new Set([item.id]));
}, 300);
}
}Core Features:
- Display multiple images
- Navigate with arrows/dots
- Swipe on mobile
- Auto-play option
- Thumbnail preview
Functional Requirements:
- Navigate between images
- Support touch gestures
- Keyboard navigation
- Lazy load images
- Responsive sizing
Non-Functional Requirements:
- Performance: Smooth 60fps animations
- Accessibility: Screen reader support
- Mobile: Touch-optimized
- Works with any number of images
┌─────────────────────────────────────────┐
│ Carousel Component │
│ ┌─────────────────────────────────┐ │
│ │ [←] [→] │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Current Image │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────┘ │ │
│ │ ● ○ ○ ○ ○ │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Thumbnail Strip (optional) │ │
│ │ [▫] [▫] [▪] [▫] [▫] │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
interface CarouselImage {
id: string;
url: string;
thumbnail?: string;
alt: string;
caption?: string;
}
interface CarouselState {
currentIndex: number;
isTransitioning: boolean;
direction: 'left' | 'right';
touchStart: number;
touchCurrent: number;
}interface CarouselProps {
images: CarouselImage[];
// Configuration
initialIndex?: number;
autoplay?: boolean;
autoplayInterval?: number; // milliseconds
loop?: boolean; // Allow wrapping
// Navigation
showArrows?: boolean;
showDots?: boolean;
showThumbnails?: boolean;
// Behavior
swipeThreshold?: number; // pixels
transitionDuration?: number; // milliseconds
// Callbacks
onSlideChange?: (index: number) => void;
onImageClick?: (image: CarouselImage) => void;
// Customization
className?: string;
arrowRenderer?: (direction: 'left' | 'right') => ReactNode;
}Implementation:
function Carousel({ images, autoplay, loop = true }) {
const [currentIndex, setCurrentIndex] = useState(0);
const [touchStart, setTouchStart] = useState(0);
const [touchEnd, setTouchEnd] = useState(0);
// Auto-play
useEffect(() => {
if (!autoplay) return;
const interval = setInterval(() => {
goToNext();
}, autoplayInterval || 3000);
return () => clearInterval(interval);
}, [currentIndex, autoplay]);
// Navigation functions
const goToNext = () => {
if (currentIndex === images.length - 1) {
if (loop) setCurrentIndex(0);
} else {
setCurrentIndex(currentIndex + 1);
}
};
const goToPrev = () => {
if (currentIndex === 0) {
if (loop) setCurrentIndex(images.length - 1);
} else {
setCurrentIndex(currentIndex - 1);
}
};
// Touch handlers
const handleTouchStart = (e) => {
setTouchStart(e.touches[0].clientX);
};
const handleTouchMove = (e) => {
setTouchEnd(e.touches[0].clientX);
};
const handleTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const threshold = 50;
if (distance > threshold) {
goToNext();
} else if (distance < -threshold) {
goToPrev();
}
setTouchStart(0);
setTouchEnd(0);
};
// Keyboard navigation
const handleKeyDown = (e) => {
if (e.key === 'ArrowLeft') goToPrev();
if (e.key === 'ArrowRight') goToNext();
};
return (
<div
className="carousel"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onKeyDown={handleKeyDown}
tabIndex={0}
>
<div className="carousel-track" style={{
transform: `translateX(-${currentIndex * 100}%)`,
transition: 'transform 300ms ease'
}}>
{images.map((image) => (
<img key={image.id} src={image.url} alt={image.alt} />
))}
</div>
<button onClick={goToPrev} aria-label="Previous">←</button>
<button onClick={goToNext} aria-label="Next">→</button>
<div className="dots">
{images.map((_, index) => (
<button
key={index}
onClick={() => setCurrentIndex(index)}
aria-label={`Go to slide ${index + 1}`}
aria-current={index === currentIndex}
/>
))}
</div>
</div>
);
}Performance:
- Lazy Loading: Only load current, previous, and next images
- Transform: Use
transform: translateX()instead ofleftfor better performance (GPU-accelerated) - Will-change: Add
will-change: transformfor smooth animations - Preload: Preload next/previous images
- Image Optimization: Responsive images with
srcset
Touch Gestures:
// Smooth swipe with momentum
const useSwipe = (onSwipeLeft, onSwipeRight) => {
const [touchStart, setTouchStart] = useState(0);
const [touchEnd, setTouchEnd] = useState(0);
const [velocity, setVelocity] = useState(0);
const minSwipeDistance = 50;
const onTouchStart = (e) => {
setTouchEnd(0);
setTouchStart(e.targetTouches[0].clientX);
};
const onTouchMove = (e) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
if (isLeftSwipe) onSwipeLeft();
if (isRightSwipe) onSwipeRight();
};
return { onTouchStart, onTouchMove, onTouchEnd };
};Accessibility:
- ARIA labels on navigation buttons
aria-currenton active dot- Keyboard navigation (arrow keys)
aria-liveregion for screen readers- Pause on focus for auto-play
User Experience:
- Zoom: Pinch to zoom on mobile
- Thumbnails: Click thumbnail to jump to image
- Captions: Show image caption
- Fullscreen: Expand to fullscreen mode
- Counter: "3 / 10" indicator
Core Features:
- Opens on trigger
- Closes on overlay click or X button
- Traps focus inside
- Prevents body scroll
- Keyboard support (Escape)
Functional Requirements:
- Display content above page
- Block interaction with background
- Animate in/out
- Support various sizes
- Accessible to screen readers
Non-Functional Requirements:
- Performance: Smooth animations
- Accessibility: WCAG 2.1 AA compliant
- Mobile: Full-screen on small devices
- Works with nested modals
┌─────────────────────────────────────────┐
│ Modal Component │
│ ┌─────────────────────────────────┐ │
│ │ Overlay (backdrop) │ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Modal Content [X] │ │ │
│ │ │ │ │ │
│ │ │ Title │ │ │
│ │ │ Body content │ │ │
│ │ │ │ │ │
│ │ │ [Cancel] [Confirm] │ │ │
│ │ └─────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
interface ModalState {
isOpen: boolean;
previousFocusElement: HTMLElement | null;
animationState: 'entering' | 'entered' | 'exiting' | 'exited';
}interface ModalProps {
// Visibility
isOpen: boolean;
onClose: () => void;
// Content
title?: string;
children: ReactNode;
footer?: ReactNode;
// Behavior
closeOnOverlayClick?: boolean; // Default: true
closeOnEscape?: boolean; // Default: true
preventScroll?: boolean; // Default: true
// Styling
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
centered?: boolean;
className?: string;
// Callbacks
onOpen?: () => void;
onAfterClose?: () => void;
}Implementation:
function Modal({ isOpen, onClose, title, children, closeOnEscape = true }) {
const overlayRef = useRef(null);
const contentRef = useRef(null);
const previousFocusRef = useRef(null);
// Store focus before opening
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement;
// Focus first focusable element
const firstFocusable = contentRef.current?.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
}
return () => {
// Restore focus when closing
previousFocusRef.current?.focus();
};
}, [isOpen]);
// Trap focus inside modal
const handleKeyDown = (e) => {
if (!isOpen) return;
if (e.key === 'Escape' && closeOnEscape) {
onClose();
return;
}
if (e.key === 'Tab') {
const focusableElements = contentRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
};
// Prevent body scroll
useEffect(() => {
if (isOpen) {
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.overflow = 'hidden';
document.body.style.paddingRight = `${scrollBarWidth}px`;
return () => {
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
}, [isOpen]);
if (!isOpen) return null;
return createPortal(
<div
ref={overlayRef}
className="modal-overlay"
onClick={(e) => {
if (e.target === overlayRef.current) {
onClose();
}
}}
onKeyDown={handleKeyDown}
>
<div
ref={contentRef}
className="modal-content"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div className="modal-header">
<h2 id="modal-title">{title}</h2>
<button
onClick={onClose}
aria-label="Close modal"
>
×
</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>,
document.body
);
}Animations:
/* Smooth fade + scale animation */
@keyframes modal-enter {
from {
opacity: 0;
transform: scale(0.95) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-content {
animation: modal-enter 200ms ease-out;
}
.modal-overlay {
animation: fade-in 200ms ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}Focus Management:
- Store focus before opening
- Move focus to modal on open
- Trap focus inside modal (Tab cycles through)
- Restore focus on close
- Focus first focusable element
Prevent Body Scroll:
// Account for scrollbar width to prevent layout shift
const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.overflow = 'hidden';
document.body.style.paddingRight = `${scrollBarWidth}px`;Accessibility:
role="dialog"on modalaria-modal="true"aria-labelledbypointing to titlearia-describedbyfor description- Focus trap
- Escape key closes
- Overlay click closes
User Experience:
- Stacking: Support multiple modals with z-index
- Confirmation: Confirm before closing if unsaved changes
- Loading States: Show spinner in modal
- Responsive: Full-screen on mobile
- Smooth Animations: Fade + scale effect
Mobile Optimization:
@media (max-width: 768px) {
.modal-content {
width: 100%;
height: 100vh;
margin: 0;
border-radius: 0;
}
}Core Features:
- Display poll question
- Multiple choice options
- Vote functionality
- Show results (bar chart)
- Real-time updates
Functional Requirements:
- Users can select one option (or multiple for multi-select)
- Submit vote
- View results after voting
- See vote count and percentages
- Prevent duplicate voting
Non-Functional Requirements:
- Performance: Updates in real-time
- Accessibility: Screen reader support
- Mobile: Touch-friendly
- Works offline (queue votes)
┌─────────────────────────────────────────┐
│ Server │
│ - Poll data │
│ - Vote storage │
│ - WebSocket for real-time │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Poll Component │
│ ┌─────────────────────────────────┐ │
│ │ Question │ │
│ │ "What's your favorite color?" │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ ○ Red [████░░░░] 45% │ │
│ │ ● Blue [██████░░] 55% │ │
│ │ ○ Green [░░░░░░░░] 0% │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ [Submit Vote] │ │
│ │ Total votes: 100 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
interface Poll {
id: string;
question: string;
options: PollOption[];
total_votes: number;
multiple_choice: boolean;
end_date?: string;
created_by: string;
}
interface PollOption {
id: string;
text: string;
votes: number;
percentage: number;
}
interface UserVote {
poll_id: string;
user_id: string;
option_ids: string[];
timestamp: number;
}Server APIs:
// GET /polls/:id - Get poll data
GET /api/polls/:id
Response: {
poll: Poll,
user_vote?: UserVote,
has_voted: boolean
}
// POST /polls/:id/vote - Submit vote
POST /api/polls/:id/vote
Body: {
option_ids: string[]
}
Response: {
poll: Poll // Updated with new counts
}
// WebSocket - Real-time updates
WS /polls/:id/subscribe
Events: {
type: 'vote_update',
poll: Poll
}Component API:
interface PollWidgetProps {
pollId: string;
// Configuration
showResults?: 'always' | 'after-vote' | 'after-end';
allowChangeVote?: boolean;
// Styling
colorScheme?: 'blue' | 'green' | 'purple';
compact?: boolean;
// Callbacks
onVote?: (optionIds: string[]) => void;
onResultsView?: () => void;
}Implementation:
function PollWidget({ pollId }) {
const [poll, setPoll] = useState(null);
const [selectedOptions, setSelectedOptions] = useState([]);
const [hasVoted, setHasVoted] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// Fetch poll data
useEffect(() => {
fetchPoll(pollId).then(data => {
setPoll(data.poll);
setHasVoted(data.has_voted);
if (data.user_vote) {
setSelectedOptions(data.user_vote.option_ids);
}
});
}, [pollId]);
// Real-time updates
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/polls/${pollId}/subscribe`);
ws.onmessage = (event) => {
const { poll: updatedPoll } = JSON.parse(event.data);
setPoll(updatedPoll);
};
return () => ws.close();
}, [pollId]);
// Submit vote
const handleVote = async () => {
if (selectedOptions.length === 0) return;
setIsSubmitting(true);
try {
const { poll: updatedPoll } = await api.vote(pollId, selectedOptions);
setPoll(updatedPoll);
setHasVoted(true);
} catch (error) {
showError('Failed to submit vote');
} finally {
setIsSubmitting(false);
}
};
// Calculate percentages
const getPercentage = (votes) => {
return poll.total_votes > 0
? Math.round((votes / poll.total_votes) * 100)
: 0;
};
return (
<div className="poll-widget">
<h3>{poll.question}</h3>
<div className="poll-options">
{poll.options.map(option => (
<div key={option.id} className="poll-option">
{!hasVoted ? (
<label>
<input
type={poll.multiple_choice ? 'checkbox' : 'radio'}
name="poll-option"
value={option.id}
checked={selectedOptions.includes(option.id)}
onChange={(e) => {
if (poll.multiple_choice) {
setSelectedOptions(prev =>
e.target.checked
? [...prev, option.id]
: prev.filter(id => id !== option.id)
);
} else {
setSelectedOptions([option.id]);
}
}}
/>
{option.text}
</label>
) : (
<div className="poll-result">
<span className="option-text">{option.text}</span>
<div className="result-bar">
<div
className="result-fill"
style={{ width: `${option.percentage}%` }}
/>
</div>
<span className="percentage">{option.percentage}%</span>
<span className="votes">({option.votes} votes)</span>
</div>
)}
</div>
))}
</div>
{!hasVoted && (
<button
onClick={handleVote}
disabled={selectedOptions.length === 0 || isSubmitting}
>
{isSubmitting ? 'Submitting...' : 'Submit Vote'}
</button>
)}
<div className="poll-footer">
Total votes: {poll.total_votes}
</div>
</div>
);
}Animations:
/* Animate result bars */
.result-fill {
transition: width 500ms ease-out;
background: linear-gradient(90deg, #4F46E5, #818CF8);
height: 100%;
border-radius: 4px;
}
/* Pulse animation for new votes */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.poll-option.new-vote {
animation: pulse 500ms ease-in-out;
}Optimistic Updates:
// Update UI immediately, rollback on failure
function submitVoteOptimistically(optionId) {
const previousPoll = { ...poll };
// Update local state
const updatedOptions = poll.options.map(opt =>
opt.id === optionId
? { ...opt, votes: opt.votes + 1 }
: opt
);
setPoll({
...poll,
options: updatedOptions,
total_votes: poll.total_votes + 1
});
// Send to server
api.vote(pollId, [optionId]).catch(() => {
// Rollback on failure
setPoll(previousPoll);
showError('Failed to submit vote');
});
}User Experience:
- Visual Feedback: Animate bar growth
- Highlight Choice: Show user's selection
- Vote Count: Real-time update
- Expired Polls: Show "Poll ended" message
- Share Results: Copy link to share
Accessibility:
- Semantic HTML (
<fieldset>,<legend>) - ARIA labels for radio/checkbox
- Keyboard navigation
- Screen reader announces results
- Color contrast for bars
Mobile Optimization:
- Touch-friendly tap targets (min 44px)
- Responsive layout
- Swipe to view more options (if scrollable)
This comprehensive guide covers 15 frontend system design questions using the RADIO framework:
- Requirements: Define scope and constraints
- Architecture: Component structure and data flow
- Data Model: Entities and their fields
- Interface Definition: APIs between components
- Optimizations: Performance, UX, accessibility
Each section provides detailed breakdowns of requirements, architecture diagrams, data models, API specifications, and optimization strategies tailored to real-world scenarios.
Key Takeaways:
- Always clarify requirements before designing
- Draw architecture diagrams to visualize components
- Define clear APIs between components
- Consider performance, accessibility, and UX
- Scale solutions appropriately to the problem─────────────────────────────────────────┐ │ Controller │ │ - Manages data flow │ │ - Handles API calls │ │ - WebSocket connection │ └─────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────┐ │ Client Store │ │ - Feed posts cache │ │ - User data │ │ - Pagination state │ └─────────────────────────────────────────┘ ↕ ┌─────────────────────────────────────────┐ │ Feed UI │ │ ┌─────────────────────────────────┐ │ │ │ Post Composer │ │ │ └─────────────────────────────────┘ │ │ ┌─────────────────────────────────┐ │ │ │ Feed Post (repeated) │ │ │ │ - Author info │ │ │ │ - Content │ │ │ │ - Interaction buttons │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────────┘
### Data Model (D)
| Source | Entity | Belongs To | Fields |
|--------|--------|------------|--------|
| Server | Post | Feed UI | `id`, `created_time`, `content`, `media_url`, `media_type`, `author` (User), `reactions` (object), `comments_count`, `shares_count` |
| Server | User | Client Store | `id`, `name`, `profile_photo_url`, `username` |
| Server | Feed | Feed UI | `posts` (Post[]), `next_cursor`, `has_more` |
| Server | Reaction | Post | `type` (like/love/haha/wow/sad/angry), `count`, `user_reacted` (boolean) |
| Client | NewPost | Post Composer | `content`, `media_file`, `is_uploading`, `upload_progress` |
| Client | UI State | Feed UI | `is_loading`, `error`, `active_post_id` |
### Interface Definition (I)
**Server APIs:**
```javascript
// GET /feed - Fetch feed posts
GET /api/feed?cursor={cursor}&size={size}
Response: {
posts: Post[],
pagination: {
next_cursor: string,
has_more: boolean
}
}
// POST /posts - Create new post
POST /api/posts
Body: {
content: string,
media?: File,
media_type?: 'image' | 'video'
}
Response: {
post: Post
}
// POST /posts/:id/reactions - React to post
POST /api/posts/:id/reactions
Body: {
type: 'like' | 'love' | 'haha' | 'wow' | 'sad' | 'angry'
}
// WebSocket events
WS /feed/updates
Events:
- new_post: { post: Post }
- post_updated: { post_id: string, changes: Partial<Post> }
Client Component APIs:
// Feed Component
interface FeedProps {
userId: string;
onPostCreated?: (post: Post) => void;
}
// Post Component
interface PostProps {
post: Post;
onReact: (type: ReactionType) => Promise<void>;
onComment: () => void;
onShare: () => void;
}
// Post Composer Component
interface PostComposerProps {
onSubmit: (content: string, media?: File) => Promise<void>;
placeholder?: string;
}Performance:
- Virtual Scrolling: Render only visible posts (react-window, react-virtualized)
- Image Lazy Loading: Load images as they enter viewport (Intersection Observer)
- Code Splitting: Lazy load video player, comment section
- Debounced Reactions: Batch reaction updates
- Optimistic UI: Immediately show reactions/comments before server confirms
Network:
- Pagination: Cursor-based pagination (better than offset for real-time feeds)
- Prefetching: Load next page when user is 80% through current page
- Request Deduplication: Cache identical API calls
- WebSocket: Real-time updates without polling
- CDN: Serve media files from CDN with appropriate cache headers
User Experience:
- Skeleton Screens: Show loading placeholders
- Pull to Refresh: Mobile gesture support
- Infinite Scroll: Automatic loading
- Error Recovery: Retry failed requests, show error boundaries
- Offline Support: IndexedDB cache, queue actions when offline
Accessibility:
- ARIA labels for all interactive elements
- Keyboard navigation (Tab, Enter, Space)
- Screen reader announcements for new posts
- Focus management for modals
- Proper heading hierarchy
Security:
- XSS Prevention: Sanitize user content
- CSRF Protection: Token-based
- Content Security Policy headers
- Rate limiting on client
Core Features:
- Real-time search suggestions as user types
- Keyboard navigation (up/down arrows, enter, escape)
- Highlight matching text
- Support for different data sources (static, API)
Functional Requirements:
- Show suggestions within 100ms of typing
- Support minimum character threshold (e.g., 3 chars)
- Handle empty results gracefully
- Clear suggestions on selection or escape
Non-Functional Requirements:
- Performance: Fast response, no janky UI
- Accessibility: Screen reader compatible
- Mobile: Touch-friendly
- Support for 1000+ items efficiently
┌─────────────────────────────────────────┐
│ Data Source │
│ - API Server / Local Data │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Controller │
│ - Debounce logic │
│ - API call management │
│ - Cache management │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Autocomplete Component │
│ ┌─────────────────────────────────┐ │
│ │ Input Field │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Suggestions Dropdown │ │
│ │ - Highlighted match │ │
│ │ - Loading indicator │ │
│ │ - Empty state │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server/Local | Suggestion | id, label, value, metadata? |
| Client | InputState | query, is_loading, error, selected_index |
| Client | Cache | Map<query, { results: Suggestion[], timestamp }> |
Server API:
GET /api/autocomplete?q={query}&limit={limit}
Response: {
suggestions: Array<{
id: string,
label: string,
value: string
}>
}Component API:
interface AutocompleteProps {
// Data source
fetchSuggestions: (query: string) => Promise<Suggestion[]>;
localData?: Suggestion[]; // For client-side filtering
// Configuration
minCharacters?: number; // Default: 3
debounceMs?: number; // Default: 300
maxResults?: number; // Default: 10
// Callbacks
onSelect: (suggestion: Suggestion) => void;
onInputChange?: (value: string) => void;
// Customization
placeholder?: string;
renderSuggestion?: (suggestion: Suggestion, query: string) => ReactNode;
}Performance:
- Debouncing: 300ms delay before API call
- Request Cancellation: Cancel previous requests (AbortController)
- Client-side Caching: Cache results for 5 minutes
- Trie Data Structure: For local filtering (O(m) lookup)
- Memoization: Memoize rendered suggestions
Network:
- Query Minimization: Only send necessary characters
- Compression: gzip responses
- HTTP/2: Multiplexing for parallel requests
User Experience:
- Loading States: Show spinner after 100ms delay
- Error Handling: Show error message, allow retry
- Empty State: "No results found" message
- Recent Searches: Show recent searches when input is empty
Accessibility:
role="combobox"on inputrole="listbox"on dropdownaria-expanded,aria-activedescendant- Keyboard navigation fully functional
- Screen reader announcements
Core Features:
- Masonry grid layout (Pinterest-style)
- Infinite scroll
- Pin creation and management
- Search and filtering
- Board organization
Functional Requirements:
- Display images in optimized masonry layout
- Users can save pins to boards
- Users can create new pins
- Search pins by keywords
- View pin details in modal
Non-Functional Requirements:
- Performance: Smooth scrolling with 1000+ images
- Images load progressively
- Mobile responsive
- Support for various image aspect ratios
┌─────────────────────────────────────────┐
│ Server (API) │
│ - Pin endpoints │
│ - Board endpoints │
│ - Image CDN │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Controller │
│ - API calls │
│ - Layout calculations │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Client Store │
│ - Pins cache │
│ - Boards │
│ - User data │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Masonry Grid Component │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Pin │ │ Pin │ │ Pin │ │
│ │ Card │ │ Card │ │ Card │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ┌─────────┐ ┌─────────┐ │
│ │ Pin │ │ Pin │ │
│ │ Card │ │ Card │ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Pin Detail Modal │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | Pin | id, image_url, title, description, author (User), board_id, width, height, created_at |
| Server | Board | id, name, description, pin_count, cover_image_url |
| Server | User | id, username, profile_image_url |
| Client | LayoutState | columns, column_heights, item_positions |
| Client | UIState | selected_pin_id, is_modal_open, scroll_position |
Server APIs:
// GET /pins - Fetch pins with pagination
GET /api/pins?cursor={cursor}&limit={limit}
Response: {
pins: Pin[],
next_cursor: string,
has_more: boolean
}
// POST /pins - Create new pin
POST /api/pins
Body: {
image_url: string,
title: string,
description?: string,
board_id: string
}
// GET /pins/:id - Get pin details
GET /api/pins/:id
Response: { pin: Pin }Component APIs:
interface MasonryGridProps {
pins: Pin[];
columnCount?: number; // Responsive: 2-7 columns
columnGap?: number; // Default: 16px
onPinClick: (pin: Pin) => void;
onLoadMore: () => void;
}
interface PinCardProps {
pin: Pin;
width: number;
onClick: () => void;
onSave: (boardId: string) => void;
}Performance:
- Virtualization: Only render visible pins (react-window-masonry)
- Image Optimization:
- Serve multiple sizes (thumbnail, medium, full)
- Use WebP with JPEG fallback
- Lazy load images with blur placeholder
- Layout Calculation: Use Web Workers for heavy calculations
- Memoization: Cache column calculations
- Intersection Observer: Load images when near viewport
Layout Algorithm:
// Masonry layout algorithm
function calculateMasonryLayout(pins, columnCount) {
const columnHeights = new Array(columnCount).fill(0);
const positions = [];
pins.forEach(pin => {
// Find shortest column
const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
positions.push({
column: shortestColumn,
top: columnHeights[shortestColumn]
});
// Update column height
const aspectRatio = pin.height / pin.width;
const pinHeight = columnWidth * aspectRatio;
columnHeights[shortestColumn] += pinHeight + gap;
});
return positions;
}Network:
- Progressive Loading: Load low-res first, then high-res
- CDN: Serve images from global CDN
- HTTP/2: Parallel image loading
- Prefetch: Preload next page of pins
User Experience:
- Skeleton Screens: Show loading placeholders
- Smooth Animations: Fade in images as they load
- Responsive: Adjust columns based on screen width
- Save on Hover: Show save button on pin hover
- Quick View: Preview pin details on hover
Accessibility:
- Alt text for all images
- Keyboard navigation between pins
- Focus indicators
- Screen reader friendly
Core Features:
- Text formatting (bold, italic, underline)
- Headings, lists (ordered/unordered)
- Links, images
- Undo/redo functionality
- Copy/paste support
Functional Requirements:
- Users can format text with toolbar buttons
- Users can use keyboard shortcuts
- Content is saved in structured format (JSON/HTML)
- Works on desktop and mobile
Non-Functional Requirements:
- Performance: Smooth typing with large documents (10k+ words)
- No lag between keypress and display
- Maintain selection across operations
┌─────────────────────────────────────────┐
│ Editor State Manager │
│ - Document tree (AST) │
│ - Selection state │
│ - History stack (undo/redo) │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Rich Text Editor Component │
│ ┌─────────────────────────────────┐ │
│ │ Toolbar │ │
│ │ [B] [I] [U] [Link] [Image] │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Editable Content Area │ │
│ │ (contenteditable div) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Formatting Controller │
│ - Apply formatting commands │
│ - Parse pasted content │
│ - Serialize to storage format │
└─────────────────────────────────────────┘
Document Structure (JSON):
{
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Hello ', marks: [] },
{ type: 'text', text: 'world', marks: [{ type: 'bold' }] }
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: 'Heading' }]
}
]
}| Entity | Fields |
|---|---|
| Node | type, attrs?, content?, marks?, text? |
| Mark | type, attrs? |
| Selection | anchor, head, type |
| EditorState | doc, selection, history |
Editor API:
interface RichTextEditorProps {
initialContent?: Doc;
onChange: (content: Doc) => void;
placeholder?: string;
readOnly?: boolean;
// Customization
enabledFormats?: FormattingOption[];
customPlugins?: Plugin[];
// Callbacks
onImageUpload?: (file: File) => Promise<string>;
onLinkCreate?: () => Promise<{ url: string, text?: string }>;
}
// Commands API
interface EditorCommands {
toggleBold: () => void;
toggleItalic: () => void;
toggleUnderline: () => void;
setHeading: (level: 1 | 2 | 3) => void;
toggleBulletList: () => void;
toggleOrderedList: () => void;
insertLink: (url: string, text?: string) => void;
insertImage: (url: string, alt?: string) => void;
undo: () => void;
redo: () => void;
}Performance:
- Efficient Re-rendering: Only update changed nodes
- Debounced Autosave: Save after 500ms of inactivity
- Virtual Scrolling: For very long documents
- Throttled onChange: Limit callback frequency
Core Implementation:
// Use contenteditable with controlled state
<div
contentEditable
onInput={handleInput}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
/>
// Handle formatting
function applyFormat(format) {
document.execCommand(format, false, null);
// OR use custom implementation for better control
}
// Keyboard shortcuts
const shortcuts = {
'Mod-b': () => commands.toggleBold(),
'Mod-i': () => commands.toggleItalic(),
'Mod-z': () => commands.undo(),
'Mod-Shift-z': () => commands.redo()
};User Experience:
- Floating Toolbar: Show on text selection
- Slash Commands: Type "/" for quick formatting
- Markdown Shortcuts: "# " for heading, "- " for list
- Drag and Drop: Images, reordering blocks
- Smart Paste: Clean formatting from Word/Google Docs
Accessibility:
- Proper ARIA roles and labels
- Keyboard navigation in toolbar
- Screen reader announces formatting changes
- Focus management
Libraries to Consider:
- ProseMirror (flexible, powerful)
- Draft.js (React-specific)
- Slate (React, customizable)
- Lexical (Meta's new editor framework)
- Quill (simpler, good for basic use cases)
Core Features:
- Real-time collaborative editing
- Rich text formatting
- Comments and suggestions
- Version history
- Offline support
Functional Requirements:
- Multiple users can edit simultaneously
- Changes propagate in real-time (<200ms latency)
- Conflict resolution for concurrent edits
- User cursors visible to all collaborators
- Auto-save every few seconds
Non-Functional Requirements:
- Performance: Handle documents up to 50k words
- Scalability: Support 50+ concurrent editors
- Reliability: No data loss, even with network issues
- Works offline with sync when reconnected
┌─────────────────────────────────────────┐
│ Server │
│ - WebSocket server │
│ - Document store │
│ - Operational Transform (OT) engine │
│ - Version history │
└─────────────────────────────────────────┘
↕ WebSocket
┌─────────────────────────────────────────┐
│ Collaboration Controller │
│ - Send local operations │
│ - Receive remote operations │
│ - Transform conflicts │
│ - Manage user presence │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Editor State │
│ - Document content │
│ - Local pending operations │
│ - Remote user cursors │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Editor UI │
│ ┌─────────────────────────────────┐ │
│ │ Toolbar & Menu │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Document Canvas │ │
│ │ - User cursors │ │
│ │ - Inline comments │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Comments Sidebar │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | Document | id, title, content (OT structure), version, owner_id, created_at, updated_at |
| Server | Operation | type, position, content, user_id, timestamp, version |
| Server | Comment | id, doc_id, position, content, author_id, resolved, thread |
| Client | UserPresence | user_id, name, color, cursor_position, selection_range |
| Client | LocalState | pending_ops, last_synced_version, offline_queue |
WebSocket Protocol:
// Client -> Server
{
type: 'operation',
op: {
type: 'insert' | 'delete' | 'retain',
position: number,
content?: string,
length?: number
},
version: number,
userId: string
}
// Server -> Client
{
type: 'operation',
op: Operation,
version: number,
userId: string,
timestamp: number
}
// Presence updates
{
type: 'presence',
userId: string,
cursor: { position: number },
selection?: { start: number, end: number }
}Editor API:
interface CollaborativeEditorProps {
documentId: string;
userId: string;
initialContent?: Doc;
// Collaboration
onCollaboratorJoin?: (user: User) => void;
onCollaboratorLeave?: (userId: string) => void;
// Comments
enableComments?: boolean;
onCommentCreate?: (comment: Comment) => void;
// Version history
onVersionRestore?: (version: number) => void;
}Operational Transformation (OT): The key challenge is handling concurrent edits. Two main approaches:
- Operational Transform (OT)
// Transform two concurrent operations
function transform(op1, op2) {
if (op1.type === 'insert' && op2.type === 'insert') {
if (op1.position < op2.position) {
op2.position += op1.content.length;
} else if (op1.position > op2.position) {
op1.position += op2.content.length;
}
}
// More cases for delete, etc.
return [op1, op2];
}- CRDT (Conflict-free Replicated Data Types)
- Alternative to OT, mathematically guaranteed convergence
- Examples: Yjs, Automerge
- Better for peer-to-peer scenarios
Performance:
- Operation Batching: Send multiple ops together
- Efficient Diffing: Only send changes, not entire document
- Lazy Loading: Load comments on demand
- Debounced Presence: Update cursor position every 100ms
- Local Echo: Show local changes immediately
Network:
- WebSocket: Persistent connection for real-time updates
- Reconnection Logic: Exponential backoff, queue operations
- Conflict Resolution: OT or CRDT for merging changes
- Compression: gzip WebSocket messages
Offline Support:
// Queue operations when offline
if (!navigator.onLine) {
offlineQueue.push(operation);
} else {
sendOperation(operation);
}
// Sync when back online
window.addEventListener('online', () => {
syncOfflineQueue();
});User Experience:
- User Avatars: Show who's editing
- Colored Cursors: Each user gets unique color
- "User is typing" Indicator
- Smart Auto-save: Save after inactivity
- Conflict Notifications: Alert user to major conflicts
- Version History: Browse and restore previous versions
Scalability:
- Sharding: Distribute documents across servers
- Read Replicas: For viewing-only users
- Caching: Redis for recent operations
- Rate Limiting: Prevent spam operations
Core Features:
- Video player with adaptive bitrate streaming
- Browse video catalog
- Search and recommendations
- Continue watching
- Multiple profiles
Functional Requirements:
- Users can play videos with controls (play/pause, seek, volume)
- Videos adapt to network conditions
- Users can browse and search content
- Progress is saved and synced across devices
- Support subtitles and multiple audio tracks
Non-Functional Requirements:
- Performance: Video starts within 2 seconds
- Quality: Adaptive bitrate (240p to 4K)
- Smooth playback without buffering
- Support all major browsers and devices
- Offline: Download for offline viewing
┌─────────────────────────────────────────┐
│ CDN / Media Server │
│ - Video segments (HLS/DASH) │
│ - Multiple bitrates │
│ - Subtitles │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ API Server │
│ - Catalog data │
│ - User profiles │
│ - Watch progress │
│ - Recommendations │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Application Controller │
│ - Fetch catalog data │
│ - Manage playback state │
│ - Track watch progress │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Client Store │
│ - Video catalog │
│ - User preferences │
│ - Playback state │
│ - Downloaded content │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Video Player Component │
│ ┌─────────────────────────────────┐ │
│ │ Video Element │ │
│ │ <video> with HLS/DASH │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Controls Overlay │ │
│ │ - Play/Pause │ │
│ │ - Progress bar │ │
│ │ - Volume │ │
│ │ - Fullscreen │ │
│ │ - Quality selector │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
↕
┌─────────────────────────────────────────┐
│ Browse UI │
│ - Hero banner │
│ - Content rows (carousels) │
│ - Search interface │
└─────────────────────────────────────────┘
| Source | Entity | Fields |
|---|---|---|
| Server | Video | id, title, description, thumbnail_url, duration, release_year, rating, genres, manifest_url (HLS/DASH) |
| Server | Episode | id, series_id, season, episode_number, title, duration, manifest_url |
| Server | WatchProgress | user_id, video_id, position, last_watched, completed |
| Server | UserProfile | id, name, avatar, preferences, watch_history |
| Client | PlaybackState | current_time, duration, is_playing, volume, playback_rate, quality, buffered_ranges |
| Client | PlayerSettings | subtitle_track, audio_track, quality_preference, autoplay |
Server APIs:
// GET /videos - Browse catalog
GET /api/videos?category={category}&limit={limit}
Response: {
videos: Video[]
}
// GET /videos/:id - Get video details
GET /api/videos/:id
Response: {
video: Video,
manifest_url: string,
subtitles: Array<{ language: string, url: string }>,
audio_tracks: Array<{ language: string, url: string }>
}
// POST /watch-progress - Update watch progress
POST /api/watch-progress
Body: {
video_id: string,
position: number,
duration: number
}
// GET /recommendations - Get personalized recommendations
GET /api/recommendations?user_id={userId}Video Player API:
interface VideoPlayerProps {
videoId: string;
manifestUrl: string;
// Configuration
autoplay?: boolean;
startPosition?: number;
// Callbacks
onTimeUpdate?: (currentTime: number) => void;
onEnded?: () => void;
onError?: (error: Error) => void;
onQualityChange?: (quality: Quality) => void;
// Customization
controls?: boolean;
subtitles?: SubtitleTrack[];
audioTracks?: AudioTrack[];
}
interface VideoPlayerMethods {
play: () => Promise<void>;
pause: () => void;
seek: (time: number) => void;
setVolume: (volume: number) => void;
setPlaybackRate: (rate: number) => void;
setQuality: (quality: string) => void;
enableSubtitle: (trackId: string) => void;
}Adaptive Bitrate Streaming:
// HLS (HTTP Live Streaming) or DASH (Dynamic Adaptive Streaming)
// Libraries: hls.js, shaka-player, video.js
const hls = new Hls({
startLevel: -1, // Auto quality
maxBufferLength: 30, // 30 seconds buffer
maxMaxBufferLength: 60
});
// Monitor network and adjust quality
hls.on(Hls.Events.MANIFEST_PARSED, () => {
const levels = hls.levels; // Available quality levels
// Switch based on bandwidth
});Performance:
- Adaptive Bitrate: Automatically switch quality based on bandwidth
- Preloading: Load first few seconds ahead
- Buffering Strategy: 30-60 seconds ahead buffer
- Lazy Load Thumbnails: Load preview images on demand
- Code Splitting: Load player library only when needed
Network:
- CDN: Serve videos from geographically close servers
- Multi-CDN: Fallback to backup CDN
- Segment Size: 2-10 second video segments
- Prefetch: Download next episode in series
- P2P Delivery: WebRTC for peer-assisted delivery
User Experience:
- Instant Play: Start with lower quality, upgrade smoothly
- Skip Intro: Detect and skip intro sequences
- Continue Watching: Resume from last position
- Autoplay Next: Countdown timer before next episode
- Thumbnail Preview: Show preview on hover over progress bar
- Keyboard Shortcuts: Space (play/pause), Arrow keys (seek), F (fullscreen)
Offline Support:
// Use Service Worker + IndexedDB for downloads
async function downloadVideo(videoId) {
const manifest = await fetch(`/api/videos/${videoId}/manifest`);
const segments = await manifest.json();
// Download all segments
for (const segment of segments) {
const response = await fetch(segment.url);
const blob = await response.blob();
await saveToIndexedDB(videoId, segment.index, blob);
}
}Accessibility:
- Closed captions/subtitles
- Audio descriptions
- Keyboard navigation
- Screen reader support for controls
- High contrast mode
Analytics:
- Track play rate, completion rate
- Monitor buffering events
- Quality switching frequency
- Error tracking
This comprehensive guide covers 15 frontend system design questions using the RADIO framework:
- Requirements: Define scope and constraints
- Architecture: Component structure and data flow
- Data Model: Entities and their fields
- Interface Definition: APIs between components
- Optimizations: Performance, UX, accessibility
Each section provides detailed breakdowns of requirements, architecture diagrams, data models, API specifications, and optimization strategies tailored to real-world scenarios.
Key Takeaways:
- Always clarify requirements before designing
- Draw architecture diagrams to visualize components
- Define clear APIs between components
- Consider performance, accessibility, and UX
- Scale solutions appropriately to the problem