Groww

Real Groww Interview Experience

4 Rounds: Take Home Assignment, TypeScript DSA, Frontend Deep Dive + Machine Coding & Hiring Manager

4
Rounds
3
Weeks Process
38 LPA
Package
SDE 2
Bangalore
Vasanth

By Vasanth Bhat

Staff Software Engineer @ Walmart Global Tech

Mentored 100+ frontend developers through successful interviews

Round 1: Take Home Assignment (3 days)

Build a financial dashboard in React. They don't just check if it works — they read every single line. Component structure, naming conventions, folder organization, and edge case handling all matter. This assignment follows you into Round 3 where they review your code live.

šŸ“Œ Build a Financial Dashboard in React

Time: 3 days | Focus: Code quality over feature count

Requirements:
• Live stock data with real-time updates
• Charts (candlestick or line chart with time range filters)
• Filters for stocks/categories
• Responsive layout (mobile + desktop)
• Code quality, component structure, naming — all reviewed
šŸŽÆ Recommended Architecture:
// Folder structure that impresses
src/
ā”œā”€ā”€ components/
│   ā”œā”€ā”€ ui/                  // Reusable primitives (Button, Card, Spinner)
│   ā”œā”€ā”€ charts/              // StockChart, CandlestickChart
│   ā”œā”€ā”€ dashboard/           // DashboardLayout, StockCard, FilterBar
│   └── watchlist/           // WatchlistPanel, WatchlistItem
ā”œā”€ā”€ hooks/
│   ā”œā”€ā”€ useStockData.ts      // WebSocket connection + data normalization
│   ā”œā”€ā”€ useDebounce.ts
│   └── useLocalStorage.ts
ā”œā”€ā”€ services/
│   ā”œā”€ā”€ stockApi.ts          // API layer (fetch + error handling)
│   └── websocket.ts         // WebSocket connection manager
ā”œā”€ā”€ types/
│   └── stock.ts             // All TypeScript interfaces
ā”œā”€ā”€ utils/
│   ā”œā”€ā”€ formatters.ts        // Currency, percentage, date formatters
│   └── constants.ts
└── context/
    └── StockContext.tsx      // Global state if needed
Key Implementation Patterns:
// Custom hook for real-time stock data
function useStockData(symbols: string[]) {
  const [stocks, setStocks] = useState<Record<string, StockData>>({});
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/stocks');
    wsRef.current = ws;

    ws.onmessage = (event) => {
      const update: StockUpdate = JSON.parse(event.data);
      setStocks(prev => ({
        ...prev,
        [update.symbol]: { ...prev[update.symbol], ...update }
      }));
    };

    ws.onopen = () => {
      ws.send(JSON.stringify({ type: 'subscribe', symbols }));
    };

    return () => {
      ws.close();
      wsRef.current = null;
    };
  }, [symbols]);

  return { stocks, isConnected: wsRef.current?.readyState === WebSocket.OPEN };
}

// Component with clean separation
function StockCard({ symbol, data, onRemove }: StockCardProps) {
  const priceChange = data.currentPrice - data.previousClose;
  const percentChange = (priceChange / data.previousClose) * 100;
  const isPositive = priceChange >= 0;

  return (
    <div className="stock-card">
      <div className="stock-card__header">
        <h3>{symbol}</h3>
        <button onClick={() => onRemove(symbol)} aria-label={`Remove ${symbol}`}>Ɨ</button>
      </div>
      <div className={`stock-card__price ${isPositive ? 'positive' : 'negative'}`}>
        <span>₹{data.currentPrice.toFixed(2)}</span>
        <span>{isPositive ? '+' : ''}{percentChange.toFixed(2)}%</span>
      </div>
    </div>
  );
}
What They Actually Review:
• Component naming and folder organization
• Separation of concerns (hooks, services, components)
• TypeScript usage — are you using any or proper types?
• Error handling — what happens when WebSocket disconnects?
• Edge cases — empty states, loading states, error states
• Cleanup — do you clean up subscriptions in useEffect?
• README — setup instructions, design decisions, trade-offs

šŸ’” Assignment Tips:

  • Quality over features: A well-structured app with 3 features beats a messy app with 10
  • Write a README:Explain your architecture decisions, trade-offs, and what you'd improve with more time
  • Add tests: Even 5-10 unit tests show you care about code quality
  • Handle edge cases: Empty states, loading, errors, network failures
  • This follows you into Round 3: Be prepared to explain EVERY decision — hook choices, component splits, folder structure

How you structure components tells Groww more than the features you built. Every naming choice, every folder split, every hook — it all gets reviewed. Learn production-grade React architecture →

Round 2: Problem Solving in TypeScript (60 mins)

DSA in TypeScript only — not JavaScript. Types and generics matter here. After every solution they ask: time complexity, space complexity, now optimize it. Most candidates prepare DSA in JS and struggle with TypeScript syntax under pressure.

1ļøāƒ£ Detect Cycle in a Linked List + Find the Starting Node

Platform: LeetCode 141 + 142 | Difficulty: Medium | Time: 15 min

Problem: Given a linked list, detect if it has a cycle. If yes, return the node where the cycle begins.šŸŽÆ Solution in TypeScript:
interface ListNode<T> {
  val: T;
  next: ListNode<T> | null;
}

// Part 1: Detect cycle (Floyd's Tortoise and Hare)
function hasCycle<T>(head: ListNode<T> | null): boolean {
  let slow = head;
  let fast = head;

  while (fast !== null && fast.next !== null) {
    slow = slow!.next;
    fast = fast.next.next;
    if (slow === fast) return true;
  }

  return false;
}

// Part 2: Find where cycle starts
function detectCycleStart<T>(head: ListNode<T> | null): ListNode<T> | null {
  let slow = head;
  let fast = head;

  // Phase 1: Find meeting point
  while (fast !== null && fast.next !== null) {
    slow = slow!.next;
    fast = fast.next.next;

    if (slow === fast) {
      // Phase 2: Find cycle start
      // Move one pointer to head, advance both by 1
      let pointer = head;
      while (pointer !== slow) {
        pointer = pointer!.next;
        slow = slow!.next;
      }
      return pointer;
    }
  }

  return null; // No cycle
}

// Why this works:
// When slow and fast meet, the distance from head to cycle start
// equals the distance from meeting point to cycle start (going around).
// Time: O(n), Space: O(1)
Follow-up: Why O(1) space matters here?
A HashSet approach uses O(n) space. Floyd's algorithm achieves the same with O(1) space — critical for large linked lists in production (e.g., detecting infinite loops in event chains).

2ļøāƒ£ LRU Cache Implementation from Scratch

Platform: LeetCode 146 | Difficulty: Hard | Time: 20 min

Problem: Implement an LRU Cache with O(1) get and put operations. Must use TypeScript with proper generics.šŸŽÆ Solution in TypeScript:
interface DLLNode<K, V> {
  key: K;
  value: V;
  prev: DLLNode<K, V> | null;
  next: DLLNode<K, V> | null;
}

class LRUCache<K, V> {
  private capacity: number;
  private cache: Map<K, DLLNode<K, V>>;
  private head: DLLNode<K, V>; // Dummy head (most recent)
  private tail: DLLNode<K, V>; // Dummy tail (least recent)

  constructor(capacity: number) {
    this.capacity = capacity;
    this.cache = new Map();

    // Initialize dummy nodes
    this.head = { key: null as K, value: null as V, prev: null, next: null };
    this.tail = { key: null as K, value: null as V, prev: null, next: null };
    this.head.next = this.tail;
    this.tail.prev = this.head;
  }

  get(key: K): V | -1 {
    const node = this.cache.get(key);
    if (!node) return -1;

    this.moveToFront(node);
    return node.value;
  }

  put(key: K, value: V): void {
    if (this.cache.has(key)) {
      const node = this.cache.get(key)!;
      node.value = value;
      this.moveToFront(node);
    } else {
      const newNode: DLLNode<K, V> = { key, value, prev: null, next: null };
      this.cache.set(key, newNode);
      this.addToFront(newNode);

      if (this.cache.size > this.capacity) {
        const lru = this.tail.prev!;
        this.removeNode(lru);
        this.cache.delete(lru.key);
      }
    }
  }

  private addToFront(node: DLLNode<K, V>): void {
    node.next = this.head.next;
    node.prev = this.head;
    this.head.next!.prev = node;
    this.head.next = node;
  }

  private removeNode(node: DLLNode<K, V>): void {
    node.prev!.next = node.next;
    node.next!.prev = node.prev;
  }

  private moveToFront(node: DLLNode<K, V>): void {
    this.removeNode(node);
    this.addToFront(node);
  }
}

// Usage
const cache = new LRUCache<number, string>(3);
cache.put(1, 'one');
cache.put(2, 'two');
cache.put(3, 'three');
cache.get(1);            // 'one' — moves to front
cache.put(4, 'four');    // Evicts key 2 (least recently used)
cache.get(2);            // -1 (evicted)

// Time: O(1) for both get and put
// Space: O(capacity)
TypeScript-specific things they notice:
• Generic types <K, V> — shows you understand generics
• Proper interface definitions for DLLNode
• Access modifiers (private) — encapsulation
• Non-null assertions (!) used judiciously vs proper null checks

3ļøāƒ£ Flatten Deeply Nested Object (No flat(), Handle Circular Refs)

Difficulty: Medium-Hard | Time: 15 min

šŸŽÆ Solution in TypeScript:
type NestedObject = Record<string, unknown>;
type FlatObject = Record<string, unknown>;

function flattenObject(
  obj: NestedObject,
  prefix: string = '',
  seen: WeakSet<object> = new WeakSet()
): FlatObject {
  const result: FlatObject = {};

  if (seen.has(obj)) {
    return { [prefix]: '[Circular Reference]' };
  }
  seen.add(obj);

  for (const key of Object.keys(obj)) {
    const newKey = prefix ? `${prefix}.${key}` : key;
    const value = obj[key];

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

  return result;
}

// Test
const input = {
  a: { b: { c: 1 } },
  d: [10, 20],
  e: { f: null, g: { h: 'deep' } }
};

console.log(flattenObject(input));
// {
//   "a.b.c": 1,
//   "d.0": 10,
//   "d.1": 20,
//   "e.f": null,
//   "e.g.h": "deep"
// }

// Circular reference handling
const circular: NestedObject = { name: 'test' };
circular.self = circular;
console.log(flattenObject(circular));
// { "name": "test", "self": "[Circular Reference]" }

// Time: O(n) where n = total number of keys at all levels
// Space: O(n) for the result object + O(d) recursion depth

šŸ’” Interview Tips for Round 2:

  • Practice DSA in TypeScript: Most people prepare in JS and freeze when asked for types under pressure
  • Use generics: LRUCache<K, V> instead of LRUCache with any
  • State complexity before coding:"This will be O(n) time, O(1) space" — then code
  • Expect "now optimize it": Always have a brute force AND optimal approach ready
  • Interface first: Define your data structures as TypeScript interfaces before implementing

Most people prepare DSA in JavaScript. Groww asks it in TypeScript. Types and generics matter here — practice accordingly. Get structured DSA practice in TypeScript →

Round 3: Frontend Deep Dive + Machine Coding (90 mins)

Starts with a live code review of your take-home assignment. They ask why you used each hook, each folder, each component split. Then moves into polyfills, React internals, and a machine coding challenge. Know every decision in your assignment.

1ļøāƒ£ Polyfills for map(), filter(), reduce()

Difficulty: Medium | Time: 10 minutes

šŸŽÆ All three polyfills:
// Array.prototype.map polyfill
Array.prototype.myMap = function<T, U>(
  callback: (value: T, index: number, array: T[]) => U,
  thisArg?: unknown
): U[] {
  const result: U[] = [];
  for (let i = 0; i < this.length; i++) {
    if (i in this) { // Handle sparse arrays
      result.push(callback.call(thisArg, this[i], i, this));
    }
  }
  return result;
};

// Array.prototype.filter polyfill
Array.prototype.myFilter = function<T>(
  callback: (value: T, index: number, array: T[]) => boolean,
  thisArg?: unknown
): T[] {
  const result: T[] = [];
  for (let i = 0; i < this.length; i++) {
    if (i in this && callback.call(thisArg, this[i], i, this)) {
      result.push(this[i]);
    }
  }
  return result;
};

// Array.prototype.reduce polyfill
Array.prototype.myReduce = function<T, U>(
  callback: (acc: U, value: T, index: number, array: T[]) => U,
  initialValue?: U
): U {
  let startIndex = 0;
  let accumulator: U;

  if (initialValue !== undefined) {
    accumulator = initialValue;
  } else {
    if (this.length === 0) {
      throw new TypeError('Reduce of empty array with no initial value');
    }
    accumulator = this[0] as unknown as U;
    startIndex = 1;
  }

  for (let i = startIndex; i < this.length; i++) {
    if (i in this) {
      accumulator = callback(accumulator, this[i], i, this);
    }
  }

  return accumulator;
};

// Tests
[1, 2, 3].myMap(x => x * 2);        // [2, 4, 6]
[1, 2, 3, 4].myFilter(x => x > 2);  // [3, 4]
[1, 2, 3, 4].myReduce((a, b) => a + b, 0); // 10
Key Details They Check:
āœ“ thisArg support via .call()
āœ“ Sparse array handling (i in this)
āœ“ reduce without initialValue uses first element
āœ“ Error for reduce on empty array with no initial value

2ļøāƒ£ Deep Clone Without JSON.stringify

Difficulty: Medium-Hard | Time: 10 minutes

Why not JSON.stringify? It fails with: functions, undefined, Dates (become strings), RegExp, circular references, Maps, Sets, and Symbol keys.
function deepClone<T>(obj: T, seen = new WeakMap()): T {
  // Primitives and null
  if (obj === null || typeof obj !== 'object') return obj;

  // Handle circular references
  if (seen.has(obj as object)) return seen.get(obj as object);

  // Handle special types
  if (obj instanceof Date) return new Date(obj.getTime()) as T;
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags) as T;
  if (obj instanceof Map) {
    const mapClone = new Map();
    seen.set(obj as object, mapClone);
    obj.forEach((val, key) => mapClone.set(deepClone(key, seen), deepClone(val, seen)));
    return mapClone as T;
  }
  if (obj instanceof Set) {
    const setClone = new Set();
    seen.set(obj as object, setClone);
    obj.forEach(val => setClone.add(deepClone(val, seen)));
    return setClone as T;
  }

  // Arrays and Objects
  const clone = (Array.isArray(obj) ? [] : {}) as T;
  seen.set(obj as object, clone);

  for (const key of Reflect.ownKeys(obj as object)) {
    (clone as Record<string | symbol, unknown>)[key] = deepClone(
      (obj as Record<string | symbol, unknown>)[key],
      seen
    );
  }

  return clone;
}

// Tests
const original = {
  date: new Date(),
  regex: /test/gi,
  nested: { a: [1, 2, { b: 3 }] },
  fn: () => 'hello', // Functions are preserved by reference
  map: new Map([['key', 'value']]),
};

const cloned = deepClone(original);
cloned.nested.a[2].b = 99;
console.log(original.nested.a[2].b); // Still 3 — deep clone works

3ļøāƒ£ useMemo/useCallback: When Removing Them Breaks the App

Difficulty: Medium | Type: Conceptual + Code

// Scenario where removing useMemo BREAKS the app:
// Infinite loop with useEffect dependency

function SearchResults({ query }: { query: string }) {
  // āœ… With useMemo — stable reference, useEffect runs only when query changes
  const filters = useMemo(() => ({ query, page: 1 }), [query]);

  // āŒ Without useMemo — new object every render → infinite loop
  // const filters = { query, page: 1 };

  const [results, setResults] = useState([]);

  useEffect(() => {
    fetchResults(filters).then(setResults);
  }, [filters]); // filters changes every render without useMemo!

  return <div>{results.map(r => <Item key={r.id} data={r} />)}</div>;
}

// Scenario where removing useCallback BREAKS the app:
// Child component with useEffect depending on parent's callback

function Parent() {
  const [count, setCount] = useState(0);

  // āœ… Stable reference — child's useEffect doesn't re-run
  const fetchData = useCallback(async () => {
    return await fetch('/api/data');
  }, []);

  // āŒ Without useCallback — child re-subscribes on every parent render
  // const fetchData = async () => fetch('/api/data');

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <DataSubscriber onFetch={fetchData} />
    </>
  );
}

function DataSubscriber({ onFetch }: { onFetch: () => Promise<Response> }) {
  useEffect(() => {
    const interval = setInterval(onFetch, 5000);
    return () => clearInterval(interval); // Cleanup + re-subscribe every render!
  }, [onFetch]); // Without useCallback, this runs EVERY render

  return <div>Subscribed</div>;
}
Key Rule: useMemo/useCallback are NOT just optimizations — they're correctness tools when values are used in dependency arrays.

4ļøāƒ£ How React Batches State Updates (and When It Doesn't)

// React 18+: Automatic batching EVERYWHERE
// All these cause only ONE re-render:

function handleClick() {
  setCount(c => c + 1);     // Batched
  setFlag(f => !f);          // Batched
  setName('updated');        // Batched
  // → Single re-render with all 3 updates
}

// Even in async code (NEW in React 18):
async function handleSubmit() {
  const data = await fetch('/api');
  setLoading(false);         // Batched (React 18+)
  setData(data);             // Batched
  // → Single re-render
}

// setTimeout — also batched in React 18+:
setTimeout(() => {
  setCount(c => c + 1);     // Batched
  setFlag(f => !f);          // Batched
}, 1000);

// When batching does NOT happen (or you want to opt out):
import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCount(c => c + 1);   // Renders immediately
  });
  // DOM is updated here
  flushSync(() => {
    setFlag(f => !f);       // Renders again
  });
  // Two separate renders
}

// Pre-React 18: Batching only in React event handlers
// setTimeout, fetch .then, native event listeners → NOT batched

5ļøāƒ£ Machine Coding: Real-Time Stock Watchlist

Difficulty: Hard | Time: 30 minutes

Requirements:
• Add/remove stocks from watchlist
• Real-time price polling (simulated every 2 seconds)
• Persist watchlist to localStorage
• Show price change indicator (green/red)
• Handle loading and error states
šŸŽÆ Solution:
import { useState, useEffect, useCallback, useRef } from 'react';

// Custom hook: localStorage sync
function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initial;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

// Custom hook: price polling
function usePricePolling(symbols: string[], interval = 2000) {
  const [prices, setPrices] = useState<Record<string, number>>({});
  const [prevPrices, setPrevPrices] = useState<Record<string, number>>({});
  const timerRef = useRef<NodeJS.Timeout>();

  const fetchPrices = useCallback(async () => {
    if (symbols.length === 0) return;
    try {
      // Simulated API — replace with real endpoint
      const newPrices: Record<string, number> = {};
      symbols.forEach(s => {
        const base = prices[s] || 100 + Math.random() * 900;
        newPrices[s] = +(base + (Math.random() - 0.5) * 10).toFixed(2);
      });

      setPrevPrices(prices);
      setPrices(newPrices);
    } catch (err) {
      console.error('Price fetch failed:', err);
    }
  }, [symbols, prices]);

  useEffect(() => {
    fetchPrices(); // Initial fetch
    timerRef.current = setInterval(fetchPrices, interval);
    return () => clearInterval(timerRef.current);
  }, [symbols.join(',')]); // Re-subscribe when symbols change

  return { prices, prevPrices };
}

// Main Component
function StockWatchlist() {
  const [watchlist, setWatchlist] = useLocalStorage<string[]>('watchlist', []);
  const [input, setInput] = useState('');
  const { prices, prevPrices } = usePricePolling(watchlist);

  const addStock = () => {
    const symbol = input.trim().toUpperCase();
    if (symbol && !watchlist.includes(symbol)) {
      setWatchlist([...watchlist, symbol]);
      setInput('');
    }
  };

  const removeStock = (symbol: string) => {
    setWatchlist(watchlist.filter(s => s !== symbol));
  };

  return (
    <div className="watchlist">
      <div className="watchlist-input">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && addStock()}
          placeholder="Add stock symbol (e.g., RELIANCE)"
        />
        <button onClick={addStock}>Add</button>
      </div>

      {watchlist.length === 0 ? (
        <p className="empty-state">No stocks in watchlist. Add one above.</p>
      ) : (
        <ul className="stock-list">
          {watchlist.map(symbol => {
            const price = prices[symbol];
            const prev = prevPrices[symbol];
            const change = price && prev ? price - prev : 0;

            return (
              <li key={symbol} className="stock-item">
                <span className="symbol">{symbol}</span>
                <span className={`price ${change > 0 ? 'up' : change < 0 ? 'down' : ''}`}>
                  {price ? `₹${price}` : 'Loading...'}
                  {change !== 0 && (
                    <span className="change">
                      {change > 0 ? 'ā–²' : 'ā–¼'} {Math.abs(change).toFixed(2)}
                    </span>
                  )}
                </span>
                <button onClick={() => removeStock(symbol)}>āœ•</button>
              </li>
            );
          })}
        </ul>
      )}
    </div>
  );
}

šŸ’” Round 3 Tips:

  • Know your assignment code by heart:They will ask "why did you use useReducer here instead of useState?"
  • Polyfills from memory: map, filter, reduce, bind, call, apply — write them on demand
  • React internals: Batching, reconciliation, fiber architecture — explain at a conceptual level
  • TypeScript debate: Know pros AND cons of migrating to TS — they want balanced thinking

Know every decision in your assignment. They will ask why you used each hook, each folder, each component split. Learn to defend your code decisions under pressure →

Round 4: Hiring Manager (30 mins)

Short. Not soft. The hiring manager wants to assess your ownership mindset, product thinking, and whether you can make pragmatic decisions. Open the Groww app before this round — "What would you change?" is not a warmup question here.

1ļøāƒ£ Walk me through a frontend problem you owned end to end in production

What they evaluate:
• Did you discover the problem or was it assigned?
• Did you scope it, design it, implement it, AND measure the outcome?
• What trade-offs did you make?
• How did you validate it worked in production? (Metrics, monitoring)
• What would you do differently if you had more time?
Structure your answer:
Discovery → Scoping → Design decisions → Implementation → Measuring impact → Reflection

2ļøāƒ£ How do you decide when to refactor vs ship?

What they want to hear:
Ship when:
• User impact is immediate and measurable
• Technical debt is contained and won't spread
• Refactoring can be done incrementally later

Refactor when:
• Velocity is declining — every change takes 3x longer
• Bugs keep recurring in the same area
• The code can't support the next 2-3 planned features
• You're about to scale the team and others will work in this code
Pro Tip:Give a real example. "We shipped feature X with known tech debt because [business reason], then refactored in the next sprint because [technical reason]. The outcome was [metric]."

3ļøāƒ£ What would you change about Groww's web experience right now?

How to prepare:
Before the interview (15 min):
• Open Groww web app on desktop AND mobile
• Go through: login → explore stocks → place a mock order → check portfolio
• Throttle network (3G in DevTools) and note performance
• Check bundle size, lighthouse score
• Note: animations, loading states, error handling, empty states
Answer framework:

"I noticed [specific issue] when [specific action]. This impacts [user metric/experience]. I'd fix it by [technical approach] because [reasoning]. Expected improvement: [quantifiable outcome]."

Areas to look at:
• Stock search — autocomplete speed, relevance
• Chart rendering — smoothness on mobile, time range switching
• Portfolio loading — skeleton states, data freshness
• Order flow — form validation, error messages, confirmation UX
• PWA experience — offline support, push notifications

4ļøāƒ£ Describe a time you pushed back on a product decision

Structure:
1. Context: What was being decided?
2. Your concern: Why did you disagree? (Technical risk, user impact, timeline)
3. How you raised it: Data, prototype, user research — not just opinion
4. Resolution: What happened? Did you convince them or commit to their decision?
5. Outcome: Were you right? Wrong? What did you learn?
Key: They want to see you can disagree professionally with data, not ego. Even a story where you were wrong but learned something is strong here.

šŸ’” Hiring Manager Tips:

  • Open the Groww app before this round:"What would you change" is their highest-signal question. Have a specific, technical answer.
  • Keep answers concise:30 minutes means 4 questions in 30 minutes. Don't monologue.
  • Show product thinking: Frame technical decisions in terms of user impact
  • Be pragmatic:"Ship then refactor" is often the right answer. Don't be a perfectionist.

What Actually Helps at Groww Interviews

DSA can be in TypeScript

Prepare accordingly. Practice linked lists, LRU cache, and tree problems in TypeScript — not just JavaScript.

If you can't explain every line, don't submit it

Your assignment follows you to Round 3. They will ask why you used each hook, each component, each file.

They judge naming, structure, and edge cases

Clean code is non-negotiable. Variable names, folder structure, error boundaries — all reviewed.

Ready to Crack Your Groww 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