
Real Groww Interview Experience
4 Rounds: Take Home Assignment, TypeScript DSA, Frontend Deep Dive + Machine Coding & Hiring Manager
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
⢠Charts (candlestick or line chart with time range filters)
⢠Filters for stocks/categories
⢠Responsive layout (mobile + desktop)
⢠Code quality, component structure, naming ā all reviewed
// 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 neededKey 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>
);
}⢠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
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
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)⢠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 checks3ļøā£ Flatten Deeply Nested Object (No flat(), Handle Circular Refs)
Difficulty: Medium-Hard | Time: 15 min
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 ofLRUCachewithany - 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
// 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); // 10Key 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
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 works3ļøā£ 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 batched5ļøā£ Machine Coding: Real-Time Stock Watchlist
Difficulty: Hard | Time: 30 minutes
⢠Real-time price polling (simulated every 2 seconds)
⢠Persist watchlist to localStorage
⢠Show price change indicator (green/red)
⢠Handle loading and error states
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
⢠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?
Discovery ā Scoping ā Design decisions ā Implementation ā Measuring impact ā Reflection
2ļøā£ How do you decide when to refactor vs ship?
⢠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
3ļøā£ What would you change about Groww's web experience right now?
⢠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
"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
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?
š” 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
Prepare accordingly. Practice linked lists, LRU cache, and tree problems in TypeScript ā not just JavaScript.
Your assignment follows you to Round 3. They will ask why you used each hook, each component, each file.
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.
