
Real Swiggy Interview Experience
4 Rounds: Online Assessment, JS + Framework, Machine Coding & System Design
Round 1: Online Assessment (60 mins) | HackerRank
3 hands-on problems focused on React components and CSS. Test cases are strict ā edge cases like empty state and API failure carry weight.
1ļøā£ Infinite Scroll Component with Data Fetching + Error Handling
Difficulty: Medium | Time: 25 minutes
Build a React component that implements infinite scroll. It must fetch data from a paginated API, display items, handle loading/error states, and automatically fetch the next page when the user scrolls near the bottom.š Requirements:
⢠Show loading spinner while fetching
⢠Display error message with retry button on failure
⢠Handle empty state (no items)
⢠Stop fetching when no more pages exist
⢠Use Intersection Observer for scroll detection (not scroll events)
import { useState, useEffect, useRef, useCallback } from 'react';
function InfiniteScroll() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [hasMore, setHasMore] = useState(true);
const observerRef = useRef(null);
const fetchItems = useCallback(async (pageNum) => {
if (loading || !hasMore) return;
setLoading(true);
setError(null);
try {
const response = await fetch(
`/api/items?page=${pageNum}&limit=20`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: Failed to fetch`);
}
const data = await response.json();
setItems(prev => [...prev, ...data.items]);
setHasMore(data.hasMore);
setPage(pageNum + 1);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [loading, hasMore]);
// Initial fetch
useEffect(() => {
fetchItems(1);
}, []);
// Intersection Observer for infinite scroll trigger
const lastItemRef = useCallback((node) => {
if (loading) return;
// Disconnect previous observer
if (observerRef.current) {
observerRef.current.disconnect();
}
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore && !error) {
fetchItems(page);
}
}, { threshold: 0.1 });
if (node) observerRef.current.observe(node);
}, [loading, hasMore, page, error, fetchItems]);
// Empty state
if (!loading && items.length === 0 && !error) {
return (
<div className="empty-state">
<p>No items found</p>
</div>
);
}
return (
<div className="infinite-scroll-container">
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<div
key={item.id}
ref={isLast ? lastItemRef : null}
className="item-card"
>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
);
})}
{loading && <div className="loader">Loading...</div>}
{error && (
<div className="error-state">
<p>Error: {error}</p>
<button onClick={() => fetchItems(page)}>
Retry
</button>
</div>
)}
{!hasMore && items.length > 0 && (
<p className="end-message">You've reached the end</p>
)}
</div>
);
}š” Key Points for Scoring:- Intersection Observer > scroll events: No throttle needed, better performance
- Callback ref pattern: Attach observer to last item dynamically
- Error recovery: Retry button re-attempts the failed page (not page 1)
- hasMore flag: Prevents unnecessary API calls after last page
- Empty state: Edge case that test cases check for
2ļøā£ Debounced Search Bar (Debounce from Scratch)
Difficulty: Medium | Time: 20 minutes
Build a search bar component that debounces API calls. Implement the debounce function from scratch (no lodash). Show search results, loading state, and handle the case where a new search should cancel the previous in-flight request.šÆ Complete Solution:
// Debounce implementation from scratch
function debounce(fn, delay) {
let timerId = null;
function debounced(...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
debounced.cancel = () => {
clearTimeout(timerId);
timerId = null;
};
return debounced;
}
// Search Bar Component with debounce
function DebouncedSearchBar() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const abortControllerRef = useRef(null);
// Create debounced search function
const debouncedSearch = useRef(
debounce(async (searchTerm) => {
if (!searchTerm.trim()) {
setResults([]);
setLoading(false);
return;
}
// Cancel previous in-flight request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
setLoading(true);
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(searchTerm)}`,
{ signal: abortControllerRef.current.signal }
);
const data = await response.json();
setResults(data.results);
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Search failed:', err);
}
} finally {
setLoading(false);
}
}, 300)
).current;
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
// Cleanup on unmount
useEffect(() => {
return () => {
debouncedSearch.cancel();
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return (
<div className="search-container">
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Search restaurants..."
/>
{loading && <div className="spinner" />}
<ul className="results-list">
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
{!loading && query && results.length === 0 && (
<p>No results found for "{query}"</p>
)}
</div>
);
}š” Key Points:- AbortController: Cancels stale requests ā prevents race conditions
- Cleanup on unmount: Cancel debounce timer + abort pending request
- encodeURIComponent: Security ā prevents injection via query params
- Empty query handling:Clear results immediately, don't make API call
3ļøā£ Responsive Image Grid with CSS Grid
Difficulty: Easy-Medium | Time: 15 minutes
Build a responsive image grid using CSS Grid that automatically adjusts the number of columns based on viewport width. Use
auto-fit and minmax ā no media queries for column count.šÆ Solution:/* Responsive grid ā no media queries needed for columns! */
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
padding: 1rem;
}
.image-card {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.image-card img {
width: 100%;
height: 200px;
object-fit: cover;
display: block;
}
.image-card .info {
padding: 1rem;
}
/* For very small screens ā single column override */
@media (max-width: 320px) {
.image-grid {
grid-template-columns: 1fr;
}
}// React component
function ResponsiveImageGrid({ images }) {
return (
<div className="image-grid">
{images.map((img) => (
<div key={img.id} className="image-card">
<img
src={img.url}
alt={img.alt}
loading="lazy"
width="280"
height="200"
/>
<div className="info">
<h4>{img.title}</h4>
<p>{img.description}</p>
</div>
</div>
))}
</div>
);
}š” Key Insights:- auto-fit vs auto-fill: auto-fit collapses empty tracks, auto-fill keeps them ā use auto-fit for responsive layouts
- minmax(280px, 1fr): Cards are minimum 280px, grow to fill remaining space equally
- No JS needed: CSS Grid handles responsive columns natively
- width/height on img: Prevents CLS (Cumulative Layout Shift)
- loading="lazy": Native lazy loading for below-fold images
š” OA Round Tips:
- Test cases are strict ā edge cases like empty state and API failure carry weight
- Always handle: loading state, error state, empty state, success state
- Use Intersection Observer (not scroll events) ā shows you know modern APIs
- Cancel stale requests with AbortController ā race condition prevention
- CSS Grid auto-fit + minmax is a must-know pattern for responsive layouts
Edge cases carry weight at Swiggy. Empty states, error recovery, and API failure handling are what separate a pass from a fail. Learn how our cohort members handle edge cases ā
Round 2: JavaScript + Framework (60 mins)
Deep JavaScript and React questions. They want you to write polyfills on screen ā definitions alone do not pass.
1ļøā£ Custom useDebounce Hook
Difficulty: Medium | Time: 10 minutes
Write a custom React hook
useDebounce that debounces a value. When the input value changes, the debounced value should only update after the specified delay has passed without further changes.šÆ Solution:function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set timer to update debounced value
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cleanup: clear timer if value changes before delay
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage in a search component:
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
// This effect only fires when debouncedSearch changes
// (500ms after user stops typing)
useEffect(() => {
if (debouncedSearch) {
fetchSearchResults(debouncedSearch);
}
}, [debouncedSearch]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}š” Why this pattern works:- Cleanup function: Each value change clears the previous timer ā only the last one fires
- Separation of concerns: Debounce logic is isolated from component logic
- Composable: Works with any value type (string, number, object)
- React-idiomatic: Uses effects correctly ā not raw setTimeout in component body
2ļøā£ Render 10,000+ Items Without Lag (Virtualization)
Difficulty: Hard | Time: 20 minutes
Only render items visible in the viewport (+ buffer). Use a library like react-window or implement from scratch.
10,000 DOM nodes = ~30MB memory, 2-3s initial render, janky scroll (15 FPS). Virtualization: ~30 DOM nodes regardless of list size = 60 FPS smooth scroll.
function VirtualizedList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const totalHeight = items.length * itemHeight;
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
items.length - 1,
Math.floor((scrollTop + containerHeight) / itemHeight)
);
// Buffer: render extra items above/below for smooth scrolling
const bufferSize = 5;
const visibleStart = Math.max(0, startIndex - bufferSize);
const visibleEnd = Math.min(items.length - 1, endIndex + bufferSize);
const visibleItems = [];
for (let i = visibleStart; i <= visibleEnd; i++) {
visibleItems.push({
item: items[i],
index: i,
style: {
position: 'absolute',
top: i * itemHeight,
height: itemHeight,
width: '100%'
}
});
}
const handleScroll = (e) => {
setScrollTop(e.currentTarget.scrollTop);
};
return (
<div
onScroll={handleScroll}
style={{ height: containerHeight, overflow: 'auto', position: 'relative' }}
>
{/* Total height spacer */}
<div style={{ height: totalHeight }}>
{visibleItems.map(({ item, index, style }) => (
<div key={item.id} style={style}>
<OrderItem data={item} />
</div>
))}
</div>
</div>
);
}
// With react-window (production approach):
import { FixedSizeList } from 'react-window';
function OrderList({ orders }) {
const Row = ({ index, style }) => (
<div style={style}>
<OrderItem data={orders[index]} />
</div>
);
return (
<FixedSizeList
height={600}
itemCount={orders.length}
itemSize={72}
width="100%"
>
{Row}
</FixedSizeList>
);
}š Performance Comparison:Without Virtualization (10,000 items):
DOM Nodes: ~30,000
Initial Render: 2,400ms
Memory Usage: ~35MB
Scroll FPS: 12-18 (janky)
With Virtualization (same 10,000 items):
DOM Nodes: ~40 (visible + buffer)
Initial Render: 12ms
Memory Usage: ~3MB
Scroll FPS: 58-60 (smooth)3ļøā£ Real-Time Order Tracker with WebSocket Reconnection
Difficulty: Hard | Time: 20 minutes
Design and implement a real-time order tracker using WebSockets. Must handle: connection setup, message parsing, reconnection with exponential backoff, and graceful degradation to polling.
"Live order tracker always comes. Lead with WebSocket reconnect logic."
function useOrderTracker(orderId) {
const [orderStatus, setOrderStatus] = useState(null);
const [riderLocation, setRiderLocation] = useState(null);
const [connectionState, setConnectionState] = useState('connecting');
const wsRef = useRef(null);
const retryCountRef = useRef(0);
const maxRetries = 5;
const connect = useCallback(() => {
const ws = new WebSocket(
`wss://api.swiggy.com/orders/${orderId}/track`
);
wsRef.current = ws;
ws.onopen = () => {
setConnectionState('connected');
retryCountRef.current = 0; // Reset retry count on success
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'STATUS_UPDATE':
setOrderStatus(data.payload);
break;
case 'RIDER_LOCATION':
setRiderLocation(data.payload);
break;
case 'ORDER_DELIVERED':
setOrderStatus({ ...data.payload, isDelivered: true });
ws.close(1000); // Clean close
break;
}
};
ws.onerror = () => {
setConnectionState('error');
};
ws.onclose = (event) => {
// Don't reconnect if intentionally closed
if (event.code === 1000) return;
setConnectionState('disconnected');
// Exponential backoff reconnection
if (retryCountRef.current < maxRetries) {
const delay = Math.min(
1000 * Math.pow(2, retryCountRef.current),
30000 // Max 30 seconds
);
retryCountRef.current++;
setTimeout(connect, delay);
} else {
// Fallback to polling after max retries
setConnectionState('polling');
startPollingFallback();
}
};
}, [orderId]);
// Polling fallback when WebSocket fails
const startPollingFallback = useCallback(() => {
const intervalId = setInterval(async () => {
try {
const response = await fetch(
`/api/orders/${orderId}/status`
);
const data = await response.json();
setOrderStatus(data.status);
setRiderLocation(data.riderLocation);
if (data.status.isDelivered) {
clearInterval(intervalId);
}
} catch (err) {
console.error('Polling failed:', err);
}
}, 5000);
return () => clearInterval(intervalId);
}, [orderId]);
useEffect(() => {
connect();
return () => {
if (wsRef.current) {
wsRef.current.close(1000);
}
};
}, [connect]);
return { orderStatus, riderLocation, connectionState };
}
// Usage:
function OrderTracker({ orderId }) {
const { orderStatus, riderLocation, connectionState } =
useOrderTracker(orderId);
return (
<div>
{connectionState === 'polling' && (
<div className="banner-warning">
Live updates unavailable. Refreshing every 5s.
</div>
)}
<OrderStatusBar status={orderStatus} />
<DeliveryMap location={riderLocation} />
</div>
);
}š” Key Design Decisions:- Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (capped) ā prevents thundering herd
- Polling fallback: Graceful degradation when WebSocket is unavailable
- Clean close (code 1000):Don't reconnect when intentionally closed (order delivered)
- Connection state: Show user-friendly messages based on connection health
- Cleanup on unmount: Close WebSocket to prevent memory leaks
4ļøā£ Polyfill of Promise.race
Difficulty: Medium | Time: 10 minutes
Implement
Promise.race from scratch. It takes an iterable of promises and returns a single promise that resolves/rejects with the value/reason of the first promise that settles.šÆ Solution:function promiseRace(promises) {
return new Promise((resolve, reject) => {
const promiseArray = Array.from(promises);
// Edge case: empty iterable ā promise never settles
// (matches native Promise.race behavior)
if (promiseArray.length === 0) return;
promiseArray.forEach((promise) => {
// Wrap in Promise.resolve to handle non-promise values
Promise.resolve(promise)
.then(resolve) // First to resolve wins
.catch(reject); // First to reject wins
});
});
}
// Test cases:
async function test() {
// Fast promise wins
const result1 = await promiseRace([
new Promise(res => setTimeout(() => res('slow'), 200)),
new Promise(res => setTimeout(() => res('fast'), 50)),
new Promise(res => setTimeout(() => res('medium'), 100)),
]);
console.log(result1); // 'fast'
// Rejection wins if it settles first
try {
await promiseRace([
new Promise(res => setTimeout(() => res('slow'), 200)),
new Promise((_, rej) => setTimeout(() => rej('error!'), 50)),
]);
} catch (e) {
console.log(e); // 'error!'
}
// Non-promise values resolve immediately
const result3 = await promiseRace([42, Promise.resolve('async')]);
console.log(result3); // 42
}
test();š” Key Insights:- First to settle wins: Whether resolve or reject ā others are ignored
- Promise.resolve() wrapper: Handles non-promise values in the array
- Empty array: Returns a forever-pending promise (native behavior)
- Use case: Timeout pattern ā race request against a timer
// Timeout any slow API call
function fetchWithTimeout(url, timeout = 5000) {
return promiseRace([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timed out')), timeout)
)
]);
}š” JS + Framework Round Tips:
- They want you to write polyfills on screen ā definitions alone do not pass
- Live order tracker always comes ā lead with WebSocket reconnect logic
- Know virtualization deeply: when to use it, how it works, trade-offs
- Custom hooks: show composition (useDebounce + useFetch = search)
- Fix the performance bug first ā hunt inline props and missing keys
Polyfills and React internals are non-negotiable at Swiggy. Writing code on screen under pressure is a different skill from knowing the answer. Practice with us ā
Round 3: Machine Coding (60 mins)
Build a Restaurant Listing App in React from scratch. State modeling decides this round ā lift cart state to context, keep filters local.
āļø Problem: Restaurant Listing App with Cart System
Time: 60 minutes
ā Filter by cuisine type
ā Sort by rating / delivery time
ā Cart system with real-time price updates
ā Error boundaries + loading skeletons
ā Mobile responsive layout
Cart ā Context (global): Multiple components need cart data (header badge, cart drawer, item cards)
Filters ā Local state:Only the listing page cares about filters ā don't pollute global state
API data ā React Query / SWR: Caching, deduplication, background refetch built-in
// ===== State Architecture =====
// CartContext.jsx ā GLOBAL state (shared across components)
const CartContext = createContext();
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM': {
const existing = state.items.find(
i => i.id === action.payload.id
);
if (existing) {
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id
? { ...i, quantity: i.quantity + 1 }
: i
)
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
}
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(i => i.id !== action.payload.id)
};
case 'UPDATE_QUANTITY': {
const { id, quantity } = action.payload;
if (quantity <= 0) {
return { ...state, items: state.items.filter(i => i.id !== id) };
}
return {
...state,
items: state.items.map(i =>
i.id === id ? { ...i, quantity } : i
)
};
}
default:
return state;
}
}
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const totalPrice = useMemo(() =>
state.items.reduce(
(sum, item) => sum + item.price * item.quantity, 0
), [state.items]
);
const totalItems = useMemo(() =>
state.items.reduce((sum, item) => sum + item.quantity, 0),
[state.items]
);
return (
<CartContext.Provider value={{ ...state, totalPrice, totalItems, dispatch }}>
{children}
</CartContext.Provider>
);
}
function useCart() {
return useContext(CartContext);
}// ===== Restaurant Listing with LOCAL filter state =====
function RestaurantListing() {
const [restaurants, setRestaurants] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Filters are LOCAL ā only this component needs them
const [cuisine, setCuisine] = useState('all');
const [sortBy, setSortBy] = useState('rating');
useEffect(() => {
async function fetchRestaurants() {
try {
setLoading(true);
const response = await fetch('/api/restaurants');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
setRestaurants(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchRestaurants();
}, []);
// Derived state ā filter + sort applied
const displayedRestaurants = useMemo(() => {
let filtered = restaurants;
if (cuisine !== 'all') {
filtered = filtered.filter(r => r.cuisine === cuisine);
}
return [...filtered].sort((a, b) => {
if (sortBy === 'rating') return b.rating - a.rating;
if (sortBy === 'deliveryTime') return a.deliveryTime - b.deliveryTime;
return 0;
});
}, [restaurants, cuisine, sortBy]);
if (loading) return <SkeletonGrid count={6} />;
if (error) return <ErrorFallback message={error} onRetry={() => window.location.reload()} />;
return (
<div>
<FilterBar
cuisine={cuisine}
onCuisineChange={setCuisine}
sortBy={sortBy}
onSortChange={setSortBy}
cuisines={[...new Set(restaurants.map(r => r.cuisine))]}
/>
<div className="restaurant-grid">
{displayedRestaurants.map(restaurant => (
<RestaurantCard key={restaurant.id} data={restaurant} />
))}
</div>
{displayedRestaurants.length === 0 && (
<p className="empty">No restaurants match your filters</p>
)}
</div>
);
}// ===== Error Boundary =====
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('ErrorBoundary caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h3>Something went wrong</h3>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
// ===== Skeleton Loading =====
function SkeletonGrid({ count = 6 }) {
return (
<div className="restaurant-grid">
{Array.from({ length: count }, (_, i) => (
<div key={i} className="skeleton-card">
<div className="skeleton-image pulse" />
<div className="skeleton-text pulse" />
<div className="skeleton-text short pulse" />
</div>
))}
</div>
);
}/* Mobile Responsive Layout */
.restaurant-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
padding: 1rem;
}
@media (max-width: 640px) {
.restaurant-grid {
grid-template-columns: 1fr;
}
}
/* Skeleton Animation */
.pulse {
animation: pulse 1.5s ease-in-out infinite;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
}
@keyframes pulse {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}šÆ Evaluation Criteria:- State Architecture: Cart in context, filters local ā shows maturity
- Error Handling: Error boundaries + fallback UI + retry mechanism
- Performance: useMemo for derived data, React.memo for cards
- UX Details: Skeleton loading (not spinner), empty states, responsive design
- Code Quality: Clean separation, reusable components, meaningful naming
š” Machine Coding Tips:
- State modeling decides this round ā cart to context, filters stay local
- Mixing state ownership fails you ā be deliberate about what goes where
- Build MVP first (fetch + display), then add features layer by layer
- Error boundaries are expected ā not optional for senior roles
- Skeleton loaders > spinners ā shows you understand perceived performance
- Mobile responsive from the start ā CSS Grid with auto-fit handles it
State architecture is the hidden test in machine coding. Where you put state reveals your seniority level. Junior: everything in one component. Senior: context for shared, local for scoped. Learn our state modeling framework ā
Round 4: Frontend System Design (60 mins)
Design Swiggy's Restaurant Page with Real-Time Order Updates. Draw boxes. Talk trade-offs. Never say "I will use Redux" without justifying why.
šļø Problem: Design Swiggy's Restaurant Page with Real-Time Order Updates
Time: 60 minutes
2. State Management Strategy
3. Data Fetching + Caching
4. Performance Optimization
5. Error Handling + Fallback UI
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā App Shell (always loaded ā navbar, cart icon, location) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā ā
ā Route-Level Code Splitting: ā
ā āāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā ā
ā ā Home Page ā ā Restaurant Page ā ā Order Track ā ā
ā ā (lazy loaded) ā ā (lazy loaded) ā ā (lazy loaded)ā ā
ā āāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā ā
ā ā
ā Restaurant Page Breakdown: ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā RestaurantHeader (image, name, rating) ā SSR ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā⤠ā
ā ā MenuCategories (tabs/accordion) ā ā
ā ā āāā MenuItem (image, name, price, add-to-cart) ā ā
ā ā āāā MenuItem ā ā
ā ā āāā MenuItem ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā⤠ā
ā ā CartDrawer (slides in from right) ā Portal ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā⤠ā
ā ā OrderStatusBar (real-time WebSocket updates) ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā// Route-level splitting
const Home = lazy(() => import('./pages/Home'));
const Restaurant = lazy(() => import('./pages/Restaurant'));
const OrderTrack = lazy(() => import('./pages/OrderTrack'));
// Component-level splitting (heavy components)
const CartDrawer = lazy(() => import('./components/CartDrawer'));
const MenuImages = lazy(() => import('./components/MenuImages'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/restaurant/:id" element={<Restaurant />} />
<Route path="/track/:orderId" element={<OrderTrack />} />
</Routes>
</Suspense>
);
}2ļøā£ State Management Strategy (with justification):// WHY this choice matters:
//
// Redux: Overkill for this. Boilerplate heavy, adds 7KB.
// Use only if you need time-travel debugging or complex
// middleware (saga for payment flows).
//
// Context + useReducer: Perfect for cart (shared, changes often).
// Native React, no library needed, good for 2-3 consumers.
//
// Zustand: Best for larger apps. 1KB, no Provider wrapping,
// works outside React (WebSocket handlers).
// Pick this if interviewer pushes for scalability.
//
// React Query: Server state (menus, restaurant data).
// Automatic caching, deduplication, background refetch.
// Decision for Swiggy Restaurant Page:
const stateStrategy = {
serverState: 'React Query', // Restaurant data, menu, reviews
clientState: 'Zustand', // Cart, UI preferences
realTimeState: 'WebSocket + Zustand', // Order tracking
formState: 'Local useState', // Search, filters (ephemeral)
};
// Zustand store for cart (justification: accessed from WebSocket handler)
import { create } from 'zustand';
const useCartStore = create((set, get) => ({
items: [],
restaurantId: null,
addItem: (item) => set((state) => {
// Prevent adding from different restaurant
if (state.restaurantId && state.restaurantId !== item.restaurantId) {
// Show "clear cart?" modal
return state;
}
const existing = state.items.find(i => i.id === item.id);
if (existing) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, qty: i.qty + 1 } : i
)
};
}
return {
items: [...state.items, { ...item, qty: 1 }],
restaurantId: item.restaurantId
};
}),
getTotal: () => {
const { items } = get();
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}
}));3ļøā£ Data Fetching + Caching:// React Query configuration
function useRestaurantMenu(restaurantId) {
return useQuery({
queryKey: ['restaurant', restaurantId, 'menu'],
queryFn: () => fetchMenu(restaurantId),
staleTime: 5 * 60 * 1000, // Menu valid for 5 min
cacheTime: 30 * 60 * 1000, // Keep in cache for 30 min
refetchOnWindowFocus: false, // Don't refetch menu on tab switch
retry: 2,
// Show stale data while refetching
placeholderData: (previousData) => previousData,
});
}
// Prefetching strategy:
// When user hovers on restaurant card ā prefetch menu
function RestaurantCard({ restaurant }) {
const queryClient = useQueryClient();
const handleHover = () => {
queryClient.prefetchQuery({
queryKey: ['restaurant', restaurant.id, 'menu'],
queryFn: () => fetchMenu(restaurant.id),
staleTime: 5 * 60 * 1000
});
};
return (
<Link
to={`/restaurant/${restaurant.id}`}
onMouseEnter={handleHover}
>
{/* card content */}
</Link>
);
}4ļøā£ Performance Optimization:// LCP Optimization ā Restaurant hero image
// 1. Preload the hero image
<link rel="preload" as="image" href={restaurant.heroImage} />
// 2. Use responsive images with srcset
<img
src={restaurant.heroImage}
srcSet={`
${restaurant.heroImage}?w=400 400w,
${restaurant.heroImage}?w=800 800w,
${restaurant.heroImage}?w=1200 1200w
`}
sizes="(max-width: 768px) 100vw, 800px"
fetchPriority="high"
alt={restaurant.name}
/>
// 3. Image CDN with auto-format (WebP/AVIF)
function getOptimizedImageUrl(url, { width, quality = 80 }) {
return `${CDN_BASE}/${url}?w=${width}&q=${quality}&f=auto`;
}
// Lazy loading for below-fold menu images
function MenuItemImage({ src, alt }) {
return (
<img
src={getOptimizedImageUrl(src, { width: 200 })}
alt={alt}
loading="lazy"
decoding="async"
width="200"
height="200"
/>
);
}
// Bundle performance budget
// Entry: < 150KB (gzipped)
// Route chunk: < 50KB each
// Total JS: < 300KB first load5ļøā£ Error Handling + Fallback UI:// Layered error handling strategy:
//
// Layer 1: React Query retry (network errors, 5xx)
// Layer 2: Error Boundary (render crashes)
// Layer 3: Fallback UI (graceful degradation)
// Layer 4: Offline support (Service Worker + cached cart)
// Offline cart preservation
function useOfflineCart() {
const cart = useCartStore();
// Persist cart to localStorage on change
useEffect(() => {
localStorage.setItem('swiggy_cart', JSON.stringify(cart.items));
}, [cart.items]);
// Restore on app load
useEffect(() => {
const saved = localStorage.getItem('swiggy_cart');
if (saved) {
try {
const items = JSON.parse(saved);
// Validate items still exist in menu (background check)
cart.restoreItems(items);
} catch (e) {
localStorage.removeItem('swiggy_cart');
}
}
}, []);
}
// Skeleton states for each section
function RestaurantPage({ id }) {
const { data: menu, isLoading, error } = useRestaurantMenu(id);
return (
<ErrorBoundary fallback={<ErrorFallback />}>
{isLoading ? (
<MenuSkeleton />
) : error ? (
<ErrorCard message="Failed to load menu" onRetry={refetch} />
) : (
<MenuList items={menu} />
)}
</ErrorBoundary>
);
}Swiggy-Specific Edge Cases to Discuss:- Offline cart: User adds items, loses connection ā cart must persist in localStorage
- Slow 3G: Progressive loading ā show restaurant info first, then menu loads
- Image LCP: Hero image is 60% of above-fold ā preload it, use CDN with auto-format
- Cross-restaurant cart:User has items from Restaurant A, tries adding from B ā show "clear cart?" modal
- Menu price changes: Item price changes between add-to-cart and checkout ā show price delta
- Flash sale traffic: 10x normal load during cricket matches + dinner time
š” Frontend System Design Tips:
- Draw boxes ā visual architecture communicates faster than words
- Talk trade-offs ā never say "I will use Redux" without justifying why over alternatives
- Talk Swiggy edge cases ā offline cart, slow 3G, image LCP, cross-restaurant cart
- Code splitting: route-level is baseline, component-level shows depth
- Caching strategy: staleTime vs cacheTime, prefetching on hover, background refetch
- Performance budget: state concrete numbers (bundle < 150KB, LCP < 2.5s)
System design at Swiggy is about justifying every decision with trade-offs. The interviewer will challenge your choices ā be ready to defend or pivot. Learn our system design framework ā
What Stands Out at Swiggy Interviews
Hunt inline props and missing keys. Show you think about render performance before adding features.
Live order tracker always comes. Lead with exponential backoff reconnection and polling fallback.
Cart to context, filters stay local. Mixing state ownership fails you. Be deliberate.
Offline cart, slow 3G image loading, cross-restaurant cart conflicts, LCP optimization.
Ready to Crack Your Swiggy Interview?
Join our cohort and get structured preparation with 1-on-1 guidance from a Staff Engineer who has mentored 100+ developers.
