4 Rounds: Technical Deep Dive, Hands-on Coding, System Design & HR
Staff Software Engineer @ Walmart Global Tech
Mentored 100+ frontend developers through successful interviews
Projects deep dive, React internals, architectural concepts, and 2 coding problems using Tries. The interviewer is not just checking if you solved it — they want to see how you think. Talk through every step, even the wrong ones.
// 1. Server receives request
// 2. Server runs React component tree
// 3. renderToString() generates HTML
// 4. HTML sent to client (user sees content!)
// 5. Client downloads JS bundle
// 6. Hydration: React attaches event listeners to existing DOM
// Server-side (Express + React)
import { renderToString } from 'react-dom/server';
import App from './App';
app.get('*', (req, res) => {
// Server renders the React tree to HTML string
const html = renderToString(<App url={req.url} />);
res.send(`
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
// Client-side (Hydration)
import { hydrateRoot } from 'react-dom/client';
import App from './App';
// hydrateRoot attaches listeners to server-rendered HTML
// instead of re-rendering the entire DOM
hydrateRoot(document.getElementById('root'), <App />);
When to Use SSR:
// Simplified Virtual DOM node structure
const vNode = {
type: 'div', // element type
props: { className: 'card' }, // attributes
children: [ // child nodes
{ type: 'h1', props: {}, children: ['Hello'] },
{ type: 'p', props: {}, children: ['World'] }
]
};
// Simplified Diffing Algorithm
function diff(oldTree, newTree) {
const patches = [];
// Rule 1: Different types → replace entire subtree
if (oldTree.type !== newTree.type) {
patches.push({ type: 'REPLACE', node: newTree });
return patches;
}
// Rule 2: Same type → diff props
const propPatches = diffProps(oldTree.props, newTree.props);
if (propPatches.length > 0) {
patches.push({ type: 'PROPS', changes: propPatches });
}
// Rule 3: Recursively diff children
const childPatches = diffChildren(
oldTree.children,
newTree.children
);
patches.push(...childPatches);
return patches;
}
function diffProps(oldProps, newProps) {
const changes = [];
// Check for changed or new props
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
changes.push({ key, value: newProps[key] });
}
}
// Check for removed props
for (const key in oldProps) {
if (!(key in newProps)) {
changes.push({ key, value: undefined });
}
}
return changes;
}
function diffChildren(oldChildren, newChildren) {
const patches = [];
const maxLen = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLen; i++) {
if (!oldChildren[i]) {
patches.push({ type: 'ADD', node: newChildren[i] });
} else if (!newChildren[i]) {
patches.push({ type: 'REMOVE', index: i });
} else {
patches.push(...diff(oldChildren[i], newChildren[i]));
}
}
return patches;
}
Key Rules of React's Reconciliation:
// ❌ Without keys — React re-renders ALL items when list changes
{items.map((item, index) => <Item key={index} data={item} />)}
// ✅ With stable keys — React only updates changed items
{items.map(item => <Item key={item.id} data={item} />)}
| Aspect | WebSockets | Polling | Long Polling |
|---|---|---|---|
| Connection | Persistent, bi-directional | New HTTP request each time | Held open until data arrives |
| Latency | Real-time (ms) | Interval-dependent (seconds) | Near real-time |
| Server Load | Persistent connections (memory) | High (repeated requests) | Moderate |
| Best For | Chat, live tracking, gaming | Dashboards, status checks | Notifications, feeds |
// WebSocket — Zomato delivery tracking
const socket = new WebSocket('wss://api.zomato.com/track');
socket.onopen = () => {
socket.send(JSON.stringify({ orderId: '12345' }));
};
socket.onmessage = (event) => {
const { lat, lng, eta } = JSON.parse(event.data);
updateDeliveryMap(lat, lng);
updateETA(eta);
};
socket.onclose = () => {
// Reconnect with exponential backoff
setTimeout(() => reconnect(), retryDelay);
};
// Polling — Restaurant availability check
function pollAvailability(restaurantId) {
const intervalId = setInterval(async () => {
const response = await fetch(
`/api/restaurants/${restaurantId}/status`
);
const { isOpen, waitTime } = await response.json();
updateUI(isOpen, waitTime);
}, 30000); // Every 30 seconds
return () => clearInterval(intervalId);
}
When to use at Zomato:
// 1. JWT (JSON Web Token) — Stateless auth
// Token stored in memory (not localStorage for security)
class AuthService {
#accessToken = null;
async login(email, password) {
const response = await fetch('/api/auth/login', {
method: 'POST',
credentials: 'include', // Send cookies
body: JSON.stringify({ email, password })
});
const { accessToken } = await response.json();
this.#accessToken = accessToken;
// Refresh token stored in HttpOnly cookie by server
}
async refreshToken() {
// Refresh token sent automatically via HttpOnly cookie
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
const { accessToken } = await response.json();
this.#accessToken = accessToken;
}
getAuthHeader() {
return { Authorization: `Bearer ${this.#accessToken}` };
}
}
// 2. Session-based — Server maintains state
// Cookie sent automatically with every request
// Simpler but harder to scale (sticky sessions)
// 3. OAuth 2.0 / PKCE — Third-party login
// Frontend redirects to Google/Facebook
// Provider redirects back with auth code
// Backend exchanges code for tokens
Security Best Practices:
Difficulty: Medium | Time: 20 minutes
class TrieNode {
constructor() {
this.children = {};
this.isEndOfWord = false;
}
}
class Trie {
constructor() {
this.root = new TrieNode();
}
// Insert word into Trie — O(m) where m = word length
insert(word) {
let node = this.root;
for (const char of word) {
if (!node.children[char]) {
node.children[char] = new TrieNode();
}
node = node.children[char];
}
node.isEndOfWord = true;
}
// Find all words with given prefix — O(p + n)
// p = prefix length, n = number of matching words
autocomplete(prefix) {
let node = this.root;
// Navigate to the prefix node
for (const char of prefix) {
if (!node.children[char]) {
return []; // No words with this prefix
}
node = node.children[char];
}
// Collect all words from this node (DFS)
const results = [];
this._collectWords(node, prefix, results);
return results;
}
_collectWords(node, currentWord, results) {
if (node.isEndOfWord) {
results.push(currentWord);
}
for (const char in node.children) {
this._collectWords(
node.children[char],
currentWord + char,
results
);
}
}
// Search if exact word exists — O(m)
search(word) {
let node = this.root;
for (const char of word) {
if (!node.children[char]) return false;
node = node.children[char];
}
return node.isEndOfWord;
}
// Check if any word starts with prefix — O(p)
startsWith(prefix) {
let node = this.root;
for (const char of prefix) {
if (!node.children[char]) return false;
node = node.children[char];
}
return true;
}
}
// Test
const trie = new Trie();
const words = ['pizza', 'pasta', 'paneer', 'burger', 'biryani', 'palak'];
words.forEach(word => trie.insert(word));
console.log(trie.autocomplete('p')); // ['pizza', 'pasta', 'paneer', 'palak']
console.log(trie.autocomplete('pa')); // ['pasta', 'paneer', 'palak']
console.log(trie.autocomplete('bi')); // ['biryani']
console.log(trie.autocomplete('z')); // []
console.log(trie.search('pizza')); // true
console.log(trie.search('piz')); // false
console.log(trie.startsWith('piz')); // true
📊 Trie Structure Visualization:
After inserting: "pizza", "pasta", "paneer"
root
/ \
p b
| |
a u/i
/ \ ...
s n
| |
t e
| |
a* e
|
r*
* = isEndOfWord
💡 Key Insights:
Difficulty: Medium | Time: 15 minutes
function groupAnagrams(strs) {
const map = new Map();
for (const str of strs) {
// Sort characters to create canonical key
// All anagrams produce the same sorted key
const key = str.split('').sort().join('');
if (!map.has(key)) {
map.set(key, []);
}
map.get(key).push(str);
}
return Array.from(map.values());
}
// Alternative: Character frequency as key (faster for long strings)
function groupAnagramsOptimal(strs) {
const map = new Map();
for (const str of strs) {
// Build frequency array (26 chars)
const freq = new Array(26).fill(0);
for (const char of str) {
freq[char.charCodeAt(0) - 97]++;
}
// Use frequency as key (e.g., "1,0,0,...,1,0,1")
const key = freq.join(',');
if (!map.has(key)) {
map.set(key, []);
}
map.get(key).push(str);
}
return Array.from(map.values());
}
// Test
console.log(groupAnagrams(["eat","tea","tan","ate","nat","bat"]));
// [["eat","tea","ate"], ["tan","nat"], ["bat"]]
💡 Key Insights:
Every answer will have a follow up — prepare to go deeper on everything. Surface-level knowledge will not pass this round. Learn how our cohort members prepare for depth →
This round caught most people off guard. Harder than Round 1. Deep React internals, JavaScript mastery, and implementation from scratch. If you have used useState or useEffect, know exactly how they work under the hood.
Difficulty: Hard | Time: 20 minutes
useState hook that maintains state across re-renders, supports functional updates, and batches state changes.
// Simulates React's internal hook system
let hooks = []; // Array to store hook states
let hookIndex = 0; // Current hook position
let component = null; // Current component being rendered
function useState(initialValue) {
const currentIndex = hookIndex;
// On first render, initialize state
// On re-renders, retrieve existing state
if (hooks[currentIndex] === undefined) {
hooks[currentIndex] =
typeof initialValue === 'function'
? initialValue() // Lazy initialization
: initialValue;
}
const setState = (newValue) => {
// Support functional updates: setState(prev => prev + 1)
const nextState = typeof newValue === 'function'
? newValue(hooks[currentIndex])
: newValue;
// Only re-render if state actually changed
if (Object.is(hooks[currentIndex], nextState)) return;
hooks[currentIndex] = nextState;
// Trigger re-render (simplified)
reRender();
};
hookIndex++;
return [hooks[currentIndex], setState];
}
function reRender() {
hookIndex = 0; // Reset hook index for new render pass
component(); // Re-execute the component function
}
// Example usage:
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Zomato');
console.log(`Count: ${count}, Name: ${name}`);
return {
increment: () => setCount(prev => prev + 1),
setName: (n) => setName(n)
};
}
// Simulate React rendering
component = Counter;
const ui = Counter(); // "Count: 0, Name: Zomato"
ui.increment(); // Triggers re-render → "Count: 1, Name: Zomato"
ui.increment(); // Triggers re-render → "Count: 2, Name: Zomato"
Why Hooks Must Be Called in Order:
// ❌ This breaks because hook positions shift between renders
function BadComponent({ showExtra }) {
const [a, setA] = useState(1); // hookIndex = 0
if (showExtra) {
const [b, setB] = useState(2); // hookIndex = 1 (sometimes!)
}
const [c, setC] = useState(3); // hookIndex = 1 or 2 ???
// React can't match hooks to their state!
}
// ✅ Always call hooks at top level
function GoodComponent({ showExtra }) {
const [a, setA] = useState(1); // Always hookIndex = 0
const [b, setB] = useState(2); // Always hookIndex = 1
const [c, setC] = useState(3); // Always hookIndex = 2
// Conditionally USE the values, not the hooks
}
💡 Key Insights:
useState(() => expensiveComputation()) runs only on first renderDifficulty: Hard | Time: 20 minutes
useEffect that runs side effects after render, supports dependency arrays, and handles cleanup functions.
🎯 Complete Implementation:
let effectHooks = []; // Store effect metadata
let effectIndex = 0; // Current effect position
function useEffect(callback, deps) {
const currentIndex = effectIndex;
const prevEffect = effectHooks[currentIndex];
// Determine if effect should run
let shouldRun = false;
if (!prevEffect) {
// First render — always run
shouldRun = true;
} else if (!deps) {
// No dependency array — run every render
shouldRun = true;
} else {
// Compare dependencies with previous render
shouldRun = deps.some(
(dep, i) => !Object.is(dep, prevEffect.deps[i])
);
}
if (shouldRun) {
// Schedule effect to run AFTER render (async)
queueMicrotask(() => {
// Run cleanup from previous effect first
if (prevEffect && prevEffect.cleanup) {
prevEffect.cleanup();
}
// Run the effect and store cleanup function
const cleanup = callback();
effectHooks[currentIndex] = { deps, cleanup };
});
}
effectIndex++;
}
// Reset for re-render
function resetEffects() {
effectIndex = 0;
}
// Unmount — run all cleanups
function unmount() {
effectHooks.forEach(effect => {
if (effect && effect.cleanup) {
effect.cleanup();
}
});
effectHooks = [];
}
// Example usage:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
// Effect with dependency — re-runs when roomId changes
useEffect(() => {
console.log(`Connecting to room: ${roomId}`);
const ws = new WebSocket(`/chat/${roomId}`);
ws.onmessage = (e) => {
setMessages(prev => [...prev, JSON.parse(e.data)]);
};
// Cleanup function — runs before next effect or unmount
return () => {
console.log(`Disconnecting from room: ${roomId}`);
ws.close();
};
}, [roomId]); // Only re-run if roomId changes
// Effect without deps — runs every render
useEffect(() => {
document.title = `${messages.length} messages`;
});
// Effect with empty deps — runs once (mount only)
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);
}
Effect Lifecycle:
Mount:
1. Component renders (DOM updated)
2. useEffect callback runs (after paint)
3. Cleanup function stored for later
Update (deps changed):
1. Component re-renders (DOM updated)
2. Previous cleanup runs FIRST
3. New effect callback runs
4. New cleanup stored
Unmount:
1. Component removed from DOM
2. All cleanup functions run
💡 Key Insights:
Type: Conceptual + Code | Time: 15 minutes
// Production Example 1: API Client Factory
function createApiClient(baseURL, authToken) {
// These are "closed over" — private to this instance
let requestCount = 0;
return {
get(endpoint) {
requestCount++;
return fetch(`${baseURL}${endpoint}`, {
headers: { Authorization: `Bearer ${authToken}` }
});
},
getRequestCount() {
return requestCount;
}
};
}
const zomatoApi = createApiClient('https://api.zomato.com', 'token123');
zomatoApi.get('/restaurants'); // Uses closed-over baseURL and authToken
console.log(zomatoApi.getRequestCount()); // 1
// Production Example 2: Rate Limiter
function createRateLimiter(maxCalls, timeWindow) {
const calls = []; // Closed over
return function(fn) {
const now = Date.now();
// Remove expired timestamps
while (calls.length && calls[0] <= now - timeWindow) {
calls.shift();
}
if (calls.length >= maxCalls) {
throw new Error('Rate limit exceeded');
}
calls.push(now);
return fn();
};
}
const limiter = createRateLimiter(5, 60000); // 5 calls per minute
// Production Example 3: Memoization
function memoize(fn) {
const cache = new Map(); // Closed over — persists between calls
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalculation = memoize((n) => {
console.log('Computing...');
return n * n;
});
expensiveCalculation(5); // "Computing..." → 25
expensiveCalculation(5); // Cache hit → 25 (no log)
Common Closure Pitfall (var in loops):
// ❌ Problem: var is function-scoped, not block-scoped
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3 (all reference same `i`)
// ✅ Fix 1: Use let (block-scoped)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
// ✅ Fix 2: Create closure with IIFE
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log(j), 100);
})(i);
}
// Output: 0, 1, 2
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes (undefined) | Yes (TDZ) | Yes (TDZ) |
| Re-declaration | Allowed | Error | Error |
| Global object | window.x = val | No | No |
// Where it ACTUALLY matters in production:
// 1. Loop closures (most common bug)
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = () => alert(i); // Always shows last value!
}
// Fix: use let → each iteration gets its own binding
// 2. Temporal Dead Zone (TDZ)
console.log(x); // undefined (var is hoisted)
var x = 5;
console.log(y); // ReferenceError! (let is in TDZ)
let y = 5;
// 3. Switch statements (block scoping matters!)
switch(action) {
case 'A':
var result = 1; // Leaks to entire function!
break;
case 'B':
var result = 2; // Re-declares (no error with var)
break;
}
// Fix: use let with explicit blocks
switch(action) {
case 'A': {
let result = 1; // Scoped to this case
break;
}
}
// React identifies hooks by CALL ORDER (index in array)
// If order changes between renders, React assigns wrong state!
// Custom Hook Example: useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Custom Hook: useFetch
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error(response.statusText);
const json = await response.json();
if (!cancelled) {
setData(json);
setError(null);
}
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchData();
return () => { cancelled = true; }; // Prevent state update after unmount
}, [url]);
return { data, loading, error };
}
// Usage in component
function RestaurantList() {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);
const { data, loading, error } = useFetch(
`/api/restaurants?q=${debouncedSearch}`
);
// Composable, testable, reusable!
}
💡 Why custom hooks are powerful:
"At Zomato's scale (millions of users on 3G/4G), I'd choose Tailwind.
1. ZERO runtime overhead — CSS-in-JS parses styles in the browser,
adding 10-20ms per component mount. With 100+ components on a
restaurant page, that's 1-2 seconds of wasted JS execution.
2. Production CSS is ~10KB (purged) vs 50-100KB for CSS-in-JS runtime.
On Jio networks at 1Mbps, that's 400ms+ saved.
3. Server-side rendering is trivial — no style extraction step, no
hydration mismatch risk.
4. Design consistency — utility classes enforce spacing/color system.
No 'margin: 13px' one-offs.
For dynamic styles, I'd use CSS variables with Tailwind:
style={{ '--progress': `${percent}%` }}
className='w-[var(--progress)]'
"
This round is where most candidates fail. Using hooks is easy — understanding how they work under the hood is what Zomato tests. See how we teach React internals →
This is not just a culture round. They expect you to defend your architecture decisions and push back if they challenge your approach. The manager round is harder than the first round — most people do not expect that.
Type: System Design | Time: 25 minutes
┌─────────────────────────────────────────────────┐
│ CLIENT (React) │
├─────────────────────────────────────────────────┤
│ Location Layer │
│ ├── Geolocation API (GPS) │
│ ├── IP-based fallback │
│ └── Manual city selection │
├─────────────────────────────────────────────────┤
│ Data Layer │
│ ├── React Query (caching + prefetching) │
│ ├── Geohash-based API calls │
│ └── Optimistic updates for favorites │
├─────────────────────────────────────────────────┤
│ Rendering Layer │
│ ├── Virtualized grid (react-window) │
│ ├── Intersection Observer (lazy images) │
│ └── Skeleton loading states │
├─────────────────────────────────────────────────┤
│ Real-time Layer │
│ ├── WebSocket: delivery ETAs │
│ ├── SSE: restaurant open/close status │
│ └── Polling: menu availability (30s) │
└─────────────────────────────────────────────────┘
│
│ HTTPS / WSS
▼
┌─────────────────────────────────────────────────┐
│ API GATEWAY (BFF) │
├─────────────────────────────────────────────────┤
│ /restaurants?lat=X&lng=Y&radius=5km │
│ /restaurants/:id/menu │
│ /restaurants/:id/availability (SSE) │
│ /track/:orderId (WebSocket) │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ BACKEND SERVICES │
├─────────────────────────────────────────────────┤
│ Spatial DB (PostGIS) │ Redis (caching) │
│ Geohash indexing │ CDN (images) │
│ Search (Elasticsearch)│ Event bus (Kafka) │
└─────────────────────────────────────────────────┘
Frontend Implementation Details:
// Location detection with fallback chain
async function getUserLocation() {
// Strategy 1: Browser Geolocation API
try {
const pos = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 5000
});
});
return { lat: pos.coords.latitude, lng: pos.coords.longitude };
} catch (e) {
// Strategy 2: IP-based geolocation (fallback)
const response = await fetch('/api/location/ip');
return response.json();
}
}
// Geohash-based API for efficient spatial queries
// Geohash converts lat/lng → string prefix for range queries
function getGeohash(lat, lng, precision = 6) {
// precision 6 ≈ 1.2km × 0.6km cell
// Nearby restaurants share geohash prefix
return encodeGeohash(lat, lng, precision);
}
// React Query for caching + prefetching
function useNearbyRestaurants(location) {
return useQuery({
queryKey: ['restaurants', location.lat, location.lng],
queryFn: () => fetchRestaurants(location),
staleTime: 5 * 60 * 1000, // Cache valid for 5 min
refetchOnWindowFocus: true, // Refresh when user returns
placeholderData: (prev) => prev // Show stale while fetching
});
}
// Optimistic filter updates
function useFilteredRestaurants(filters) {
const queryClient = useQueryClient();
// Apply filters client-side from cached data (instant)
// while fetching server-filtered results in background
return useQuery({
queryKey: ['restaurants', filters],
queryFn: () => fetchWithFilters(filters),
placeholderData: () => {
const allRestaurants = queryClient.getQueryData(['restaurants']);
return applyFiltersLocally(allRestaurants, filters);
}
});
}
Schema Design (asked in follow-up):
-- Restaurants table with spatial indexing
restaurants:
id: UUID (PK)
name: VARCHAR
location: POINT (lat, lng) -- PostGIS spatial type
geohash: VARCHAR(8) -- For prefix-based range queries
cuisine_type: VARCHAR[]
avg_rating: DECIMAL
is_open: BOOLEAN
delivery_radius_km: INT
avg_delivery_time_min: INT
-- Spatial index for "nearby" queries
CREATE INDEX idx_restaurant_location ON restaurants
USING GIST (location);
-- Geohash index for partition-based lookups
CREATE INDEX idx_restaurant_geohash ON restaurants (geohash);
-- Query: Find restaurants within 5km
SELECT * FROM restaurants
WHERE ST_DWithin(location, ST_MakePoint(77.5946, 12.9716), 5000)
AND is_open = true
ORDER BY avg_rating DESC
LIMIT 20;
// S — Single Responsibility Principle
// Each component/function does ONE thing
// ❌ Bad: Component fetches, filters, AND renders
function RestaurantPage() {
const [data, setData] = useState([]);
const [filters, setFilters] = useState({});
useEffect(() => { /* fetch logic */ }, []);
const filtered = data.filter(/* filter logic */);
return (/* complex render logic */);
}
// ✅ Good: Separated concerns
function RestaurantPage() {
const { data } = useRestaurants(); // Data fetching hook
const filtered = useFilteredData(data); // Filter logic hook
return <RestaurantGrid items={filtered} />; // Render only
}
// O — Open/Closed Principle
// Open for extension, closed for modification
// ✅ Plugin-based filter system
const filterStrategies = {
price: (items, value) => items.filter(i => i.price <= value),
rating: (items, value) => items.filter(i => i.rating >= value),
cuisine: (items, value) => items.filter(i => i.cuisine === value),
// Add new filters without modifying existing code!
distance: (items, value) => items.filter(i => i.distance <= value),
};
function applyFilters(items, activeFilters) {
return Object.entries(activeFilters).reduce(
(result, [key, value]) => filterStrategies[key](result, value),
items
);
}
// L — Liskov Substitution Principle
// Subtypes must be usable wherever parent type is expected
// ✅ All button variants work with same props interface
function PrimaryButton({ onClick, children, ...props }) {
return <button className="btn-primary" onClick={onClick} {...props}>{children}</button>;
}
function GhostButton({ onClick, children, ...props }) {
return <button className="btn-ghost" onClick={onClick} {...props}>{children}</button>;
}
// Both can replace <Button /> without breaking parent component
// I — Interface Segregation Principle
// Don't force components to depend on props they don't use
// ❌ Bad: Card gets entire restaurant object (100+ fields)
<RestaurantCard restaurant={fullRestaurantObject} />
// ✅ Good: Card receives only what it renders
<RestaurantCard
name={restaurant.name}
rating={restaurant.rating}
imageUrl={restaurant.imageUrl}
deliveryTime={restaurant.deliveryTime}
/>
// D — Dependency Inversion Principle
// Depend on abstractions, not concrete implementations
// ✅ Analytics service abstracted behind interface
const analyticsService = {
track: (event, data) => { /* implementation */ }
};
// Component depends on abstraction, not specific analytics SDK
function OrderButton({ analytics = analyticsService }) {
const handleClick = () => {
analytics.track('order_placed', { restaurantId });
};
}
| Level | Dirty Read | Non-Repeatable Read | Phantom Read | Use Case |
|---|---|---|---|---|
| Read Uncommitted | ✗ Possible | ✗ Possible | ✗ Possible | Analytics (approximate OK) |
| Read Committed | ✓ Prevented | ✗ Possible | ✗ Possible | Most web apps (PostgreSQL default) |
| Repeatable Read | ✓ Prevented | ✓ Prevented | ✗ Possible | Financial reports, inventory |
| Serializable | ✓ Prevented | ✓ Prevented | ✓ Prevented | Payments, seat booking |
1. Restaurant seat/table booking (Zomato Dining):
→ Serializable: Two users booking the last table must
not both succeed. Need strictest isolation.
2. Order placement:
→ Repeatable Read: During checkout, prices and availability
should not change mid-transaction.
3. Restaurant listing page:
→ Read Committed: OK if another user's review appears
after page load. Eventual consistency acceptable.
4. Analytics dashboard (restaurant partner):
→ Read Uncommitted: Approximate order counts are fine.
Performance > precision for dashboards.
Frontend implications:
// Component Architecture
// ─────────────────────
// RestaurantListingPage (route-level, code-split)
// ├── FilterBar (sticky, client-state)
// │ ├── CuisineFilter
// │ ├── PriceFilter
// │ ├── RatingFilter
// │ └── SortDropdown
// ├── RestaurantGrid (virtualized)
// │ └── RestaurantCard (memoized)
// │ ├── LazyImage (intersection observer)
// │ ├── RatingBadge
// │ ├── DeliveryInfo
// │ └── OfferTag
// ├── InfiniteScrollTrigger (intersection observer)
// └── SkeletonLoader (SSR placeholder)
// Performance Strategy:
const RestaurantListingPage = lazy(() =>
import('./RestaurantListingPage')
);
// Data fetching strategy
function useRestaurantListing(location, filters) {
return useInfiniteQuery({
queryKey: ['restaurants', location, filters],
queryFn: ({ pageParam = 0 }) =>
fetchRestaurants({ ...filters, offset: pageParam, limit: 20 }),
getNextPageParam: (lastPage) => lastPage.nextOffset,
staleTime: 2 * 60 * 1000,
// Prefetch next page for instant scroll
onSuccess: (data) => {
const lastPage = data.pages[data.pages.length - 1];
if (lastPage.nextOffset) {
queryClient.prefetchInfiniteQuery(/* next page */);
}
}
});
}
// Image loading strategy
// 1. LQIP (Low Quality Image Placeholder) — 20 byte base64
// 2. Intersection Observer triggers full image load
// 3. Progressive JPEG for perceived speed
// 4. WebP with JPEG fallback (picture element)
// Critical rendering path:
// 1. SSR: Server renders first 6 restaurant cards (above fold)
// 2. Skeleton: Below-fold shows animated placeholders
// 3. Hydration: React takes over, enables interactivity
// 4. Lazy load: Images and below-fold content load on scroll
// Bundle size budget in CI
// webpack.config.js
module.exports = {
performance: {
maxAssetSize: 250000, // 250KB per chunk
maxEntrypointSize: 500000, // 500KB entry
hints: 'error' // Fail build if exceeded
}
};
// Lighthouse CI in GitHub Actions
// Fail PR if performance score drops below 90
The manager round tests if you think like a senior engineer. Defending your decisions under pressure is the invisible skill that separates SDE 2 from SDE 1. Learn how we prepare for this →
Compensation negotiation, motivation assessment, and relocation logistics.
It shows you think like a senior. Jumping straight into code signals junior thinking.
Every answer will have a follow up. If you explain SSR, expect "what are the trade-offs?" immediately after.
Do not write anything you cannot explain under pressure. They will dig into every project listed.
Most people don't expect that. Prepare system design and architectural defense at a higher bar than the technical rounds.
Join our cohort and get structured preparation with 1-on-1 guidance from a Staff Engineer who has mentored 100+ developers.