Zepto

Real Zepto Interview Experience

3 Rounds: JavaScript Deep Dive, Machine Coding in React & Hiring Manager

3
Rounds
2
Weeks Process
30-40 LPA
Package
SDE 2
Bangalore
Vasanth

By Vasanth Bhat

Staff Software Engineer @ Walmart Global Tech

Mentored 100+ frontend developers through successful interviews

Round 1: JavaScript Deep Dive (60 mins)

Started with a project deep dive, then moved into heavy JavaScript fundamentals. The interviewer wants to see that you truly understand the language — not just surface-level syntax but closures, prototypes, async patterns, and data manipulation.

1️⃣ Implement Infinite Curry: sum(1)(2)(3)() returns 6

Difficulty: Medium | Time: 10 minutes

Problem Statement:
Create a function sum that can be called with any number of chained invocations. When called with no arguments (or empty parentheses), it returns the total sum of all previous arguments.
• sum(1)(2)(3)() → 6
• sum(5)(10)() → 15
• sum(1)(2)(3)(4)(5)() → 15
🎯 Solution:
function sum(a) {
  return function inner(b) {
    if (b === undefined) {
      return a;
    }
    return sum(a + b);
  };
}

// How it works:
// sum(1) returns inner, which has closure over a=1
// inner(2) calls sum(1+2) = sum(3), returns new inner with a=3
// inner(3) calls sum(3+3) = sum(6), returns new inner with a=6
// inner() — b is undefined, returns a = 6

console.log(sum(1)(2)(3)());      // 6
console.log(sum(5)(10)());         // 15
console.log(sum(1)(2)(3)(4)(5)()); // 15
Key Concepts:
✓ Closures — each invocation captures the running total
✓ Recursion — sum calls itself with accumulated value
✓ Function as return value — enables chaining

2️⃣ Generic Deep Compare Function

Difficulty: Medium-Hard | Time: 15 minutes

Problem Statement:
Implement a function that deeply compares any two values — primitives, objects, arrays, nested structures, and mixed types. Return true if they are structurally equal.🎯 Solution:
function deepEqual(a, b) {
  // 1. Strict equality (handles primitives, null, undefined, same ref)
  if (a === b) return true;

  // 2. If either is null/undefined or not an object, they're not equal
  if (a == null || b == null) return false;
  if (typeof a !== 'object' || typeof b !== 'object') return false;

  // 3. Handle Date objects
  if (a instanceof Date && b instanceof Date) {
    return a.getTime() === b.getTime();
  }

  // 4. Handle RegExp
  if (a instanceof RegExp && b instanceof RegExp) {
    return a.toString() === b.toString();
  }

  // 5. Handle Arrays
  if (Array.isArray(a) !== Array.isArray(b)) return false;

  // 6. Compare keys
  const keysA = Object.keys(a);
  const keysB = Object.keys(b);

  if (keysA.length !== keysB.length) return false;

  // 7. Recursively compare each key
  for (const key of keysA) {
    if (!keysB.includes(key)) return false;
    if (!deepEqual(a[key], b[key])) return false;
  }

  return true;
}

// Tests
deepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } }); // true
deepEqual([1, [2, 3]], [1, [2, 3]]);                         // true
deepEqual({ a: 1 }, { a: 1, b: 2 });                         // false
deepEqual(new Date('2024-01-01'), new Date('2024-01-01'));    // true
Why Zepto Asks This:
✓ Tests recursive thinking
✓ Edge case handling (Date, RegExp, null, arrays vs objects)
✓ Real-world use: State comparison, test assertions, memoization

3️⃣ Polyfill: Promise.all & Promise.allSettled

Difficulty: Medium | Time: 15 minutes

Problem Statement:
Write polyfills for both Promise.all and Promise.allSettled from scratch.🎯 Promise.all Polyfill:
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Input must be an array'));
    }

    const results = [];
    let completed = 0;
    const total = promises.length;

    if (total === 0) return resolve([]);

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          results[index] = value; // Maintain order
          completed++;
          if (completed === total) {
            resolve(results);
          }
        })
        .catch(reject); // First rejection rejects the whole thing
    });
  });
}

// Test
promiseAll([
  Promise.resolve(1),
  Promise.resolve(2),
  Promise.resolve(3)
]).then(console.log); // [1, 2, 3]
🎯 Promise.allSettled Polyfill:
function promiseAllSettled(promises) {
  return new Promise((resolve) => {
    if (!Array.isArray(promises)) {
      return resolve([]);
    }

    const results = [];
    let completed = 0;
    const total = promises.length;

    if (total === 0) return resolve([]);

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          results[index] = { status: 'fulfilled', value };
        })
        .catch((reason) => {
          results[index] = { status: 'rejected', reason };
        })
        .finally(() => {
          completed++;
          if (completed === total) {
            resolve(results);
          }
        });
    });
  });
}

// Test
promiseAllSettled([
  Promise.resolve(1),
  Promise.reject('error'),
  Promise.resolve(3)
]).then(console.log);
// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: 'error' },
//   { status: 'fulfilled', value: 3 }
// ]
Key Difference:
Promise.all — short-circuits on first rejection
Promise.allSettled — waits for all, never rejects

4️⃣ Execution Order: setTimeout, Promise, async/await

Difficulty: Medium | Time: 10 minutes

Question:What's the output order and why?
async function foo() {
  console.log('1');
  await Promise.resolve();
  console.log('2');
}

console.log('3');

setTimeout(() => console.log('4'), 0);

foo();

new Promise((resolve) => {
  console.log('5');
  resolve();
}).then(() => console.log('6'));

console.log('7');
Output: 3, 1, 5, 7, 2, 6, 4

Step-by-step breakdown:
1. console.log('3') — synchronous, runs first
2. setTimeout — scheduled as macrotask
3. foo() called → console.log('1') — synchronous
4. await pauses foo, rest of foo goes to microtask queue
5. Promise executor runs synchronously → console.log('5')
6. .then callback queued as microtask
7. console.log('7') — synchronous
8. Call stack empty → microtasks: '2' (from await), then '6' (from .then)
9. Macrotask: '4' (setTimeout)
Key Rule: Sync → Microtasks (Promises, await) → Macrotasks (setTimeout, setInterval)

5️⃣ Closures: Implement once() and memoize() with Cache Limit

Difficulty: Medium | Time: 10 minutes

once() — Function runs only once:
function once(fn) {
  let called = false;
  let result;

  return function (...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

// Usage
const initialize = once(() => {
  console.log('Initializing...');
  return 'done';
});

initialize(); // logs "Initializing...", returns "done"
initialize(); // returns "done" (no log)
initialize(); // returns "done" (no log)
memoize() with LRU cache limit:
function memoize(fn, limit = 10) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      // Move to end (most recently used)
      const value = cache.get(key);
      cache.delete(key);
      cache.set(key, value);
      return value;
    }

    const result = fn.apply(this, args);

    // Evict oldest if over limit
    if (cache.size >= limit) {
      const firstKey = cache.keys().next().value;
      cache.delete(firstKey);
    }

    cache.set(key, result);
    return result;
  };
}

// Usage
const expensiveCalc = memoize((n) => {
  console.log('Computing...');
  return n * n;
}, 3); // Cache limit of 3

expensiveCalc(2); // Computing... → 4
expensiveCalc(3); // Computing... → 9
expensiveCalc(2); // 4 (cached, no computation)
expensiveCalc(4); // Computing... → 16
expensiveCalc(5); // Computing... → 25 (evicts oldest)
Why This Matters:
✓ once() — prevents duplicate API calls, initialization logic
✓ memoize() — performance optimization for expensive computations
✓ LRU eviction — memory management in real applications

6️⃣ Flatten Deeply Nested Object with Circular Reference Handling

Difficulty: Hard | Time: 15 minutes

Problem Statement:
Flatten a deeply nested object into a single-level object with dot-notation keys. Handle circular references without infinite loops.
Input: { a: { b: { c: 1 } }, d: [2, 3] }
Output: { "a.b.c": 1, "d.0": 2, "d.1": 3 }
🎯 Solution:
function flattenObject(obj, prefix = '', seen = new WeakSet()) {
  const result = {};

  // Circular reference check
  if (typeof obj === 'object' && obj !== null) {
    if (seen.has(obj)) {
      return { [prefix]: '[Circular]' };
    }
    seen.add(obj);
  }

  for (const key in obj) {
    if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;

    const newKey = prefix ? `${prefix}.${key}` : key;
    const value = obj[key];

    if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
      Object.assign(result, flattenObject(value, newKey, seen));
    } else {
      result[newKey] = value;
    }
  }

  return result;
}

// Test
const obj = { a: { b: { c: 1 } }, d: [2, 3], e: { f: null } };
console.log(flattenObject(obj));
// { "a.b.c": 1, "d.0": 2, "d.1": 3, "e.f": null }

// Circular reference test
const circular = { name: 'test' };
circular.self = circular;
console.log(flattenObject(circular));
// { "name": "test", "self": "[Circular]" }
Key Points:
✓ WeakSet tracks visited objects (allows GC, no memory leak)
✓ Handles arrays (indexed as dot notation)
✓ Handles null, Date, and other edge cases

7️⃣ Prototypes: __proto__ vs prototype vs Object.create

Difficulty: Medium | Time: Conceptual

Question: Explain the difference between __proto__, prototype, and Object.create().
// 1. prototype — property on FUNCTIONS (constructor functions)
// It's the blueprint for instances created with 'new'
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  return this.name + ' makes a sound';
};

// 2. __proto__ — property on OBJECTS (instances)
// Points to the prototype of the constructor that created it
const dog = new Animal('Rex');
console.log(dog.__proto__ === Animal.prototype); // true
console.log(dog.__proto__.__proto__ === Object.prototype); // true

// 3. Object.create() — creates object with specified prototype
// No constructor function needed
const proto = {
  greet() { return 'Hello, ' + this.name; }
};

const person = Object.create(proto);
person.name = 'Alice';
console.log(person.greet()); // "Hello, Alice"
console.log(person.__proto__ === proto); // true

// KEY DIFFERENCES:
// prototype   → lives on functions, shared by all instances
// __proto__   → lives on objects, points UP the chain (deprecated, use Object.getPrototypeOf)
// Object.create → creates new object with a given prototype, cleaner than new
Interview Tip: Draw the prototype chain on paper. Show that dog.__proto__Animal.prototypeObject.prototypenull. Interviewers love visual explanations.

💡 Interview Tips for Round 1:

  • Think out loud: Explain your approach before writing code
  • Start with edge cases: Mention them upfront — shows senior thinking
  • Know the "why":Not just how closures work, but when you'd use them in production
  • Be ready for follow-ups: Every answer leads to a deeper question
  • Practice writing from scratch: No autocomplete in interviews

Zepto's Round 1 is pure JavaScript mastery. If you can write these from scratch without hesitation, you're ready. Master JavaScript fundamentals with structured practice →

Round 2: Machine Coding in React (90 mins)

Real product scenarios with multiple follow-ups. You build a component, then they keep adding requirements. The goal is to see how you structure code under evolving requirements — extensibility matters.

📌 Build a Toast Notification System

Difficulty: Hard | Time: 90 minutes (progressive requirements)

Initial Requirements:
• Multiple variants: success, error, warning, info
• Auto-dismiss after configurable duration
• Queue system — max 3 visible at a time
• Custom positioning (top-right, bottom-left, etc.)
• Smooth enter/exit animations
🎯 Base Solution:
// toast-context.jsx
import { createContext, useContext, useReducer, useCallback, useRef } from 'react';

const ToastContext = createContext(null);

const TOAST_LIMIT = 3;
const DEFAULT_DURATION = 3000;

function toastReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return {
        ...state,
        toasts: [...state.toasts, action.payload],
        queue: state.toasts.length >= TOAST_LIMIT
          ? [...state.queue, action.payload]
          : state.queue,
      };
    case 'REMOVE':
      const remaining = state.toasts.filter(t => t.id !== action.id);
      // Pull from queue if available
      if (state.queue.length > 0 && remaining.length < TOAST_LIMIT) {
        const [next, ...restQueue] = state.queue;
        return { toasts: [...remaining, next], queue: restQueue };
      }
      return { ...state, toasts: remaining };
    default:
      return state;
  }
}

export function ToastProvider({ children, position = 'top-right' }) {
  const [state, dispatch] = useReducer(toastReducer, { toasts: [], queue: [] });
  const toastId = useRef(0);

  const addToast = useCallback(({ message, variant = 'info', duration = DEFAULT_DURATION }) => {
    const id = ++toastId.current;
    dispatch({ type: 'ADD', payload: { id, message, variant, duration } });

    if (duration > 0) {
      setTimeout(() => {
        dispatch({ type: 'REMOVE', id });
      }, duration);
    }

    return id;
  }, []);

  const removeToast = useCallback((id) => {
    dispatch({ type: 'REMOVE', id });
  }, []);

  return (
    <ToastContext.Provider value={{ addToast, removeToast }}>
      {children}
      <ToastContainer toasts={state.toasts} position={position} onDismiss={removeToast} />
    </ToastContext.Provider>
  );
}

export const useToast = () => useContext(ToastContext);
Follow-up 1: No re-rendering the entire app on every toast
// Solution: Move toast state OUTSIDE React using a store pattern
// Only the ToastContainer subscribes to changes

let listeners = [];
let toasts = [];

const toastStore = {
  getState: () => toasts,
  subscribe: (listener) => {
    listeners.push(listener);
    return () => { listeners = listeners.filter(l => l !== listener); };
  },
  addToast: (toast) => {
    toasts = [...toasts, { ...toast, id: Date.now() }];
    listeners.forEach(l => l());
  },
  removeToast: (id) => {
    toasts = toasts.filter(t => t.id !== id);
    listeners.forEach(l => l());
  }
};

// Only ToastContainer subscribes — rest of app never re-renders
function ToastContainer() {
  const [state, setState] = useState(toastStore.getState());

  useEffect(() => {
    return toastStore.subscribe(() => setState(toastStore.getState()));
  }, []);

  return (
    <div className="toast-container">
      {state.slice(0, TOAST_LIMIT).map(toast => (
        <Toast key={toast.id} {...toast} onDismiss={() => toastStore.removeToast(toast.id)} />
      ))}
    </div>
  );
}

// Any component can trigger toasts without prop drilling or context re-renders
// import { toastStore } from './toast-store';
// toastStore.addToast({ message: 'Saved!', variant: 'success' });
Follow-up 2: Programmatic dismiss via ref
import { useRef, useImperativeHandle, forwardRef } from 'react';

// Expose dismiss method via ref
const ToastManager = forwardRef((props, ref) => {
  const [toasts, setToasts] = useState([]);

  useImperativeHandle(ref, () => ({
    dismiss: (id) => {
      setToasts(prev => prev.filter(t => t.id !== id));
    },
    dismissAll: () => {
      setToasts([]);
    },
    add: (toast) => {
      const id = Date.now();
      setToasts(prev => [...prev, { ...toast, id }]);
      return id; // Return id for programmatic dismiss
    }
  }));

  return (
    <div className="toast-container">
      {toasts.map(t => <Toast key={t.id} {...t} />)}
    </div>
  );
});

// Usage in parent
function App() {
  const toastRef = useRef();

  const handleSave = async () => {
    const loadingId = toastRef.current.add({
      message: 'Saving...',
      variant: 'info',
      duration: 0 // persistent
    });

    await saveData();

    // Programmatically dismiss loading toast
    toastRef.current.dismiss(loadingId);
    toastRef.current.add({ message: 'Saved!', variant: 'success' });
  };

  return (
    <>
      <button onClick={handleSave}>Save</button>
      <ToastManager ref={toastRef} />
    </>
  );
}
What They're Evaluating:
• Component architecture — can you separate concerns cleanly?
• Performance awareness — do you think about re-renders?
• API design — is your component easy for other devs to use?
• Adaptability — can you extend without rewriting?

📌 Variant: Build an OTP Input Component

Difficulty: Medium | Time: 30 minutes

Requirements:
• 6 individual input boxes
• Auto-focus next box on input
• Backspace moves to previous box
• Paste support — distributes digits across boxes
• onComplete callback when all boxes filled
🎯 Solution:
import { useState, useRef, useCallback } from 'react';

function OTPInput({ length = 6, onComplete }) {
  const [otp, setOtp] = useState(new Array(length).fill(''));
  const inputRefs = useRef([]);

  const handleChange = useCallback((e, index) => {
    const value = e.target.value;
    if (!/^\d*$/.test(value)) return; // Only digits

    const newOtp = [...otp];
    // Take only last character (handles overwrite)
    newOtp[index] = value.slice(-1);
    setOtp(newOtp);

    // Auto-focus next input
    if (value && index < length - 1) {
      inputRefs.current[index + 1].focus();
    }

    // Check if complete
    const otpString = newOtp.join('');
    if (otpString.length === length && onComplete) {
      onComplete(otpString);
    }
  }, [otp, length, onComplete]);

  const handleKeyDown = useCallback((e, index) => {
    if (e.key === 'Backspace') {
      if (!otp[index] && index > 0) {
        // If current box empty, move to previous
        inputRefs.current[index - 1].focus();
        const newOtp = [...otp];
        newOtp[index - 1] = '';
        setOtp(newOtp);
      }
    }
  }, [otp]);

  const handlePaste = useCallback((e) => {
    e.preventDefault();
    const pastedData = e.clipboardData.getData('text').slice(0, length);
    if (!/^\d+$/.test(pastedData)) return;

    const newOtp = [...otp];
    pastedData.split('').forEach((digit, i) => {
      newOtp[i] = digit;
    });
    setOtp(newOtp);

    // Focus last filled or next empty
    const focusIndex = Math.min(pastedData.length, length - 1);
    inputRefs.current[focusIndex].focus();

    if (pastedData.length === length && onComplete) {
      onComplete(pastedData);
    }
  }, [otp, length, onComplete]);

  return (
    <div style={{ display: 'flex', gap: '8px' }}>
      {otp.map((digit, index) => (
        <input
          key={index}
          ref={(el) => (inputRefs.current[index] = el)}
          type="text"
          inputMode="numeric"
          maxLength={1}
          value={digit}
          onChange={(e) => handleChange(e, index)}
          onKeyDown={(e) => handleKeyDown(e, index)}
          onPaste={handlePaste}
          style={{
            width: '48px', height: '48px',
            textAlign: 'center', fontSize: '1.5rem',
            border: '2px solid #ddd', borderRadius: '8px'
          }}
        />
      ))}
    </div>
  );
}

📌 Variant: Type Ahead Search with Debounce & Keyboard Navigation

Difficulty: Medium-Hard | Time: 40 minutes

Requirements:
• Debounced API calls (300ms)
• Cancel previous pending requests
• Keyboard navigation (↑/↓ to navigate, Enter to select)
• Loading and empty states
• Click outside to close dropdown
🎯 Solution:
import { useState, useEffect, useRef, useCallback } from 'react';

function useDebounce(value, delay) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounced;
}

function TypeAheadSearch({ fetchSuggestions, onSelect }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const [isOpen, setIsOpen] = useState(false);
  const abortRef = useRef(null);
  const containerRef = useRef(null);

  const debouncedQuery = useDebounce(query, 300);

  // Fetch results
  useEffect(() => {
    if (!debouncedQuery.trim()) {
      setResults([]);
      setIsOpen(false);
      return;
    }

    // Cancel previous request
    if (abortRef.current) abortRef.current.abort();
    abortRef.current = new AbortController();

    setLoading(true);
    fetchSuggestions(debouncedQuery, abortRef.current.signal)
      .then((data) => {
        setResults(data);
        setIsOpen(true);
        setActiveIndex(-1);
      })
      .catch((err) => {
        if (err.name !== 'AbortError') setResults([]);
      })
      .finally(() => setLoading(false));
  }, [debouncedQuery, fetchSuggestions]);

  // Click outside to close
  useEffect(() => {
    const handleClick = (e) => {
      if (containerRef.current && !containerRef.current.contains(e.target)) {
        setIsOpen(false);
      }
    };
    document.addEventListener('mousedown', handleClick);
    return () => document.removeEventListener('mousedown', handleClick);
  }, []);

  const handleKeyDown = useCallback((e) => {
    if (!isOpen) return;

    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(prev => Math.min(prev + 1, results.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(prev => Math.max(prev - 1, 0));
        break;
      case 'Enter':
        if (activeIndex >= 0) {
          onSelect(results[activeIndex]);
          setIsOpen(false);
          setQuery(results[activeIndex].label);
        }
        break;
      case 'Escape':
        setIsOpen(false);
        break;
    }
  }, [isOpen, results, activeIndex, onSelect]);

  return (
    <div ref={containerRef} style={{ position: 'relative' }}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        onKeyDown={handleKeyDown}
        onFocus={() => results.length > 0 && setIsOpen(true)}
        placeholder="Search..."
      />
      {loading && <span className="spinner" />}
      {isOpen && results.length > 0 && (
        <ul className="dropdown">
          {results.map((item, i) => (
            <li
              key={item.id}
              className={i === activeIndex ? 'active' : ''}
              onClick={() => { onSelect(item); setIsOpen(false); setQuery(item.label); }}
              onMouseEnter={() => setActiveIndex(i)}
            >
              {item.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

💡 Machine Coding Tips for Zepto:

  • Start with the simplest working version: Get it rendering first, then add features
  • Think about extensibility: They WILL add follow-ups, so keep code flexible
  • Separate logic from UI: Custom hooks, stores, or reducers — show you can decouple
  • Handle edge cases: Rapid clicks, network failures, race conditions
  • Name things well: Clean variable names show you write production code

Machine coding at Zepto is all about building real product features under time pressure. Practice building 2-3 components end-to-end every week. Get weekly machine coding practice with expert feedback →

Round 3: Hiring Manager (45 mins)

Project deep dive + behavioral + product thinking. This is not a soft round — they're evaluating your depth of ownership, communication clarity, and whether you'd fit the team culture at Zepto.

1️⃣ Walk me through your most challenging frontend project

What they expect:
• They will dig 3 levels deep into every claim
• "You said you improved performance — what exactly? How did you measure?"
• "You mentioned you led the project — what decisions did YOU make vs the team?"
• If you can't go deep, they assume you weren't the real owner
How to structure your answer (STAR+):

Situation: What was the product/feature, team size, your role
Task: What was the specific technical challenge
Action: What YOU did — architecture decisions, trade-offs considered, alternatives rejected
Result: Quantifiable outcome (metrics improved, bugs reduced, time saved)
+ Reflection: What would you do differently now?

2️⃣ A real performance bug you fixed in production

Framework for answering:
1. Detection: How did you discover it? (Monitoring, user reports, profiling)
2. Diagnosis: Root cause analysis — what tools did you use? (DevTools, Lighthouse, React Profiler)
3. Fix: What was the solution? Why that approach over alternatives?
4. Validation: How did you verify it was fixed? What metrics improved?
5. Prevention: What did you put in place to prevent it happening again?
Example stories they love:
• Fixed a re-render cascade that caused 500ms lag on every keystroke
• Identified a memory leak from event listeners in a SPA
• Reduced bundle size by 40% by analyzing and splitting chunks
• Fixed a hydration mismatch causing layout shift in production

3️⃣ Tell me about a time you disagreed with product or design

What they're looking for:

✓ You can push back with data, not ego
✓ You understand business impact of technical decisions
✓ You can collaborate even when disagreeing
✓ You know when to commit vs when to escalate
Avoid:Stories where you were clearly wrong, stories where you're the lone hero, or stories where you just gave in without discussion.

Best: Show a nuanced situation where you backed your position with evidence, listened to the other side, and the team reached a better outcome.

4️⃣ What would you change about the Zepto app right now?

How to prepare:
Before the interview:
• Open the Zepto app and use it for 15 minutes
• Place a real order (or go through the flow)
• Note: loading states, transitions, error handling, search UX
• Check performance on a slow network (3G throttle)
• Compare with Blinkit/Swiggy Instamart
Example answer structure:

"I noticed [specific issue] when [specific action]. This impacts [user metric]. I'd improve it by [technical approach] because [reasoning]. The expected impact would be [measurable outcome]."

Good areas to observe:
• Search experience (autocomplete speed, relevance)
• Category browsing (virtualization, image loading)
• Cart management (optimistic updates, sync)
• Delivery tracking (real-time updates, map performance)
• Skeleton loading and perceived performance

5️⃣ Why Zepto over other quick commerce companies?

Framework for a genuine answer:
1. Product:What specifically excites you about quick commerce / Zepto's approach?
2. Scale: What technical challenges at Zepto interest you? (Real-time, performance at scale)
3. Growth: Why is this the right time to join? (Stage of company, engineering culture)
4. Team: What have you heard about the engineering team? (Blog posts, talks, tech stack choices)
Pro Tip:Never badmouth competitors. Instead of "Zepto is better than Blinkit because...", say "What draws me to Zepto specifically is..."

💡 Hiring Manager Round Tips:

  • Use the app before this round: 15 minutes minimum. Have one specific improvement ready with a technical approach.
  • Prepare 2-3 deep projects: Ones where you can explain architecture decisions, trade-offs, and outcomes at 3 levels of depth.
  • Quantify everything:"Improved performance" means nothing. "Reduced LCP from 3.2s to 1.1s" means everything.
  • Show ownership:"I decided" > "We decided" > "The team decided"
  • Be honest:If you don't know something, say so. Faking it at this level is obvious.

What Actually Helps at Zepto Interviews

Learn why your code works, not just how

They dig deep into fundamentals. Surface-level knowledge won't survive the follow-ups.

Practice writing functions from scratch

No IDE help in interviews. You should be able to write curry, memoize, debounce, Promise polyfills on a whiteboard.

Time yourself building components

90 minutes goes fast with follow-ups. Practice building toast, OTP, autocomplete in 30 minutes each.

Speak your thoughts while coding

Silent coding is a red flag. Narrate your thinking — trade-offs, alternatives considered, edge cases.

Keep one project ready to explain in depth

Architecture decisions, performance fixes, technical trade-offs. They will ask 3 levels of "why".

Use the Zepto app before the manager round

15 minutes of real usage. Note one improvement you'd make with a specific technical approach.

Ready to Crack Your Zepto Interview?

Join our cohort and get structured preparation with 1-on-1 guidance from a Staff Engineer who has mentored 100+ developers.

Join Next CohortContact Us
← Back to Interview Experiences