Swiggy

Real Swiggy Interview Experience

4 Rounds: Online Assessment, JS + Framework, Machine Coding & System Design

4
Rounds
React
+ Performance
30+ LPA
Package
SDE 2
Bangalore
Vasanth

By Vasanth Bhat

Staff Software Engineer @ Walmart Global Tech

Mentored 100+ frontend developers through successful interviews

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

Problem Statement:
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:
• Fetch data from paginated API (/api/items?page=1&limit=20)
• 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)
🎯 Complete Solution:
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

Problem Statement:
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

Problem Statement:
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

Problem Statement:
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

Expected Answer: Windowing / Virtualization
Only render items visible in the viewport (+ buffer). Use a library like react-window or implement from scratch.
Why not just render all 10,000?
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.
🎯 Implementation from Scratch:
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

Problem Statement:
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.
Interviewer Note:
"Live order tracker always comes. Lead with WebSocket reconnect logic."
🎯 Complete Implementation:
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

Problem Statement:
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
Practical Use — Request Timeout:
// 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

📝 Requirements:
✓ Fetch & display restaurants from a mock API
✓ Filter by cuisine type
✓ Sort by rating / delivery time
✓ Cart system with real-time price updates
✓ Error boundaries + loading skeletons
✓ Mobile responsive layout
State Modeling Strategy (This Decides the Round):

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
🎯 Architecture & Implementation:
// ===== 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:
  1. State Architecture: Cart in context, filters local — shows maturity
  2. Error Handling: Error boundaries + fallback UI + retry mechanism
  3. Performance: useMemo for derived data, React.memo for cards
  4. UX Details: Skeleton loading (not spinner), empty states, responsive design
  5. 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

Key Areas to Cover:
1. Component Architecture + Code Splitting
2. State Management Strategy
3. Data Fetching + Caching
4. Performance Optimization
5. Error Handling + Fallback UI
1️⃣ Component Architecture + Code Splitting:
┌──────────────────────────────────────────────────────────────┐
│ 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 load
5️⃣ 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

Fix the performance bug first

Hunt inline props and missing keys. Show you think about render performance before adding features.

JS Round: WebSocket reconnect logic

Live order tracker always comes. Lead with exponential backoff reconnection and polling fallback.

Machine Coding: State ownership

Cart to context, filters stay local. Mixing state ownership fails you. Be deliberate.

System Design: Swiggy edge cases

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.

Join Next Cohort