A candidate who went through Zomato's SDE-1 loop described the online round running on Intervue rather than HackerRank: four problems in ninety minutes, an easy warmup followed by something closer to a Dijkstra shortest-path question that most people in the batch didn't finish in time. That shape, one throwaway question and one that actually separates candidates, shows up often enough across reports that it reads like a deliberate design rather than one proctor's habit.
The rounds after the OA follow a pattern candidates describe consistently: a first technical round built around data structures, SQL, and core CS fundamentals, a second round that mixes more DSA with a system design problem, and a closing round with an engineering manager that leans behavioral. Zomato's Glassdoor page has accumulated close to 800 interview questions and more than 700 reviews across roles, which is enough volume that patterns repeat rather than looking like one-off anecdotes (Glassdoor, Zomato Interview Questions).
What makes the loop distinct from a generic product-company interview is how much of the system design and case-study material ties directly back to food delivery mechanics: restaurant reopening notifications, delivery partner availability by area, order volume estimation, cancellation-rate tracking. Product and business roles get their own version of this, usually a market-sizing question dressed up as a business case rather than a coding problem. Employees on Blind have also flagged fast hiring paired with fast turnover on some teams, worth knowing before you optimize purely for getting the offer.
Online assessment (coding test)
The OA is typically four problems on Intervue, ninety minutes, ranging from an easy array or string warmup to something that requires a real algorithm, not just brute force with better variable names.
Easy questions
15Binary search twice, once biased toward the left boundary and once biased toward the right, instead of scanning linearly once you've found a single match. The trick in both searches is what you do when you find a value equal to the target: for the left boundary you keep searching left even after a match, for the right boundary you keep searching right.
def search_range(nums, target):
def find(is_left):
lo, hi, result = 0, len(nums) - 1, -1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
result = mid
if is_left:
hi = mid - 1
else:
lo = mid + 1
elif nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return result
return [find(True), find(False)]O(log n) each way, O(log n) total, versus O(n) for a linear scan. Interviewers usually ask you to also handle the target not existing at all, which should return [-1, -1].
If both fit in memory, sort the smaller one and binary search into it for each element of the larger, or just build a hash set from one and check membership from the other, whichever is faster to reason about under time pressure. If one array is too large to hold in memory, sort both externally and merge with two pointers, reading each file sequentially, which keeps memory bounded to whatever buffer size you choose.
def intersect(a, b):
seen = set(a)
return [x for x in b if x in seen]The follow-up about the array too large for memory is really testing whether you know external merge sort exists, not whether you can write it from scratch in the interview.
This is Kadane's algorithm: track the best sum ending at the current position, and reset it to the current element alone whenever the running sum drops below the element itself, since carrying a negative running total forward only ever hurts the next sum.
def max_subarray(nums):
best = current = nums[0]
for n in nums[1:]:
current = max(n, current + n)
best = max(best, current)
return bestThis gets asked as a warmup more than a challenge, but interviewers watch whether you explain why the reset rule works instead of reciting it, since the same logic underlies harder variants like max product subarray asked elsewhere in the same loop.
Run a traversal, breadth-first or depth-first, from every unvisited node, and count how many times you have to start a fresh traversal. Each fresh start corresponds to one connected component, since anything reachable from that node gets marked visited in the same pass.
def count_components(n, edges):
adj = {i: [] for i in range(n)}
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
visited = set()
count = 0
for node in range(n):
if node not in visited:
count += 1
stack = [node]
while stack:
curr = stack.pop()
if curr not in visited:
visited.add(curr)
stack.extend(adj[curr])
return countUnion-Find is the other standard approach and works just as well here. Either is fine as long as you can state the time complexity and don't confuse a connected components count with a shortest-path or cycle-detection question, which candidates occasionally do under pressure.
A join between orders and restaurants, a date filter, a group by, and a limit with an order by descending count. The part worth being deliberate about out loud is whether ties at the boundary should all be included, which changes limit into a window function.
SELECT r.restaurant_id, r.name, COUNT(o.order_id) AS order_count
FROM restaurants r
JOIN orders o ON o.restaurant_id = r.restaurant_id
WHERE o.order_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY r.restaurant_id, r.name
ORDER BY order_count DESC
LIMIT 3;Normalization splits data into multiple tables to eliminate redundancy, one row per fact, joined together when you need the full picture. Denormalization deliberately reintroduces redundancy, storing derived or duplicated data together, to avoid joins on the read path.
For an order-history table specifically, you'd denormalize the restaurant name and cuisine type directly onto each order row rather than joining to the restaurants table every time, because order history is read far more often than restaurant details change, and a restaurant renaming itself shouldn't rewrite years of historical orders anyway. The trade-off is you accept some duplicated data and a small write-time cost to keep it consistent, in exchange for faster, simpler reads at scale.
A LEFT JOIN from restaurants to ratings filtered to one-star, checking for a null on the ratings side, or equivalently a NOT EXISTS. Both express the same set logic; NOT EXISTS tends to read more clearly once the filter condition gets more complex than a single equality.
SELECT r.restaurant_id, r.name
FROM restaurants r
WHERE NOT EXISTS (
SELECT 1 FROM ratings rt
WHERE rt.restaurant_id = r.restaurant_id AND rt.stars = 1
);A load balancer distributes incoming traffic across multiple instances of the same service so no single instance gets overwhelmed, and it operates mostly at the connection or request level without caring what the request actually means. An API gateway sits in front of many different services and handles routing to the correct one, plus cross-cutting concerns like authentication, rate limiting, and request logging in one place instead of duplicating that logic inside every individual service.
In practice you need both because they solve different problems at different layers: the gateway decides which service a request belongs to, and the load balancer, sitting in front of that specific service, decides which instance of it handles this particular request.
A shared cache, not direct service-to-service calls. Direct calls create a tight coupling where one service's availability and latency become a dependency of the other, and if three or four services all need the same rating, you've now got a small mesh of point-to-point calls instead of one clear source. A shared cache in front of the ratings database lets every consumer read the same value independently, with the cache absorbing read load so the underlying database isn't hit for the same rating repeatedly across services.
Click-through on the row itself is a weak signal on its own, since it measures curiosity, not value delivered. The metric that actually matters is whether orders originating from that row convert at a comparable or better rate than orders from search or browsing, and whether users exposed to the row order more overall than a control group who never saw it, not just whether they clicked something inside it.
The trap to avoid is cannibalization, the row could show great click and order numbers while simply pulling orders that would have happened anyway through search, producing no net lift. A proper A/B test comparing total order volume between users who see the row and users who don't is what actually answers whether the feature adds value or just moves it around.
Pick a decision with a real trade-off attached, not just a feature you built. Interviewers here specifically probe resume claims, one candidate reported being asked to defend a Django templating choice in detail rather than just describing what the project did at a high level. Name the alternative you considered, why you didn't pick it, and what you'd do differently with what you know now.
The weakest version of this answer describes what the project accomplished without ever mentioning a choice that could have gone the other way. The strongest version picks one moment where two reasonable engineers might have disagreed, and explains your reasoning for the side you landed on.
A genuine answer here connects something specific about the role, the food-delivery domain's scale and logistics problems, real-time systems at high concurrency, to work you've actually done, rather than reciting the company's mission statement back at the interviewer. The second half of the question is the more interesting one to answer honestly: naming a real concern, pace of change, how performance gets evaluated, and asking about it directly shows you've done homework beyond the careers page.
Employee reviews on sites like Blind sometimes mention fast hiring paired with fast attrition on certain teams. Bringing that up thoughtfully, not as an accusation but as something you'd like to understand better, tends to land better than pretending you haven't seen any of it.
Give a real range grounded in market data for the role and level, not a number pulled out of thin air, and be direct about whether you're anchoring to your current package plus a specific increase or to what the market for this role in this city actually pays. Vague deflection here just gets a follow-up question, not a better outcome.
Answer the actual reason, not a diplomatically sanded-down version that says nothing. Growth ceiling, scope you want that you can't get where you are, a specific technical direction you want to move toward, all of these are honest and reasonable. What reads poorly is an answer so vague it could apply to leaving any job for any reason, since it signals you either haven't thought it through or aren't being straight about it.
State your actual contractual notice period plainly, and if there's genuine flexibility, whether you can negotiate it down with your current employer, say so along with any conditions attached. This is a logistics question, not a test, and the only wrong answer is being unclear about something that's usually written down in your existing offer letter.
Medium questions
23Keep two heaps, a max-heap for the lower half of the numbers seen so far and a min-heap for the upper half, and rebalance them so their sizes never differ by more than one. Every new number goes into one heap based on comparison with the current top values, then you rebalance if needed. The median is either the top of the larger heap, or the average of both tops if the heaps are equal size.
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap, store negatives
self.hi = [] # min-heap
def add(self, num):
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
if len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def median(self):
if len(self.lo) > len(self.hi):
return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2Each insert costs O(log n) against a naive O(n log n) re-sort, which is the whole point when the stream keeps growing and you need the median after every element, not just once at the end.
For the basic check, a frequency count of characters is enough, either a fixed-size array for lowercase letters or a hash map for anything wider. For the substring version, you're really doing a sliding window of the pattern's length across the text, and comparing frequency counts at each position instead of recomputing from scratch.
def find_anagrams(s, p):
if len(p) > len(s):
return []
p_count = [0] * 26
s_count = [0] * 26
for ch in p:
p_count[ord(ch) - ord('a')] += 1
result = []
for i in range(len(s)):
s_count[ord(s[i]) - ord('a')] += 1
if i >= len(p):
s_count[ord(s[i - len(p)]) - ord('a')] -= 1
if i >= len(p) - 1 and s_count == p_count:
result.append(i - len(p) + 1)
return resultSliding the window and adjusting counts incrementally keeps this at O(n), against O(n * m) for re-checking every window from scratch.
Treat every order as a start event and an end event, sort all events by time, and walk through them incrementing a counter on a start and decrementing on an end, tracking the maximum value the counter ever reaches. Ties matter here, an order ending at the same minute another starts shouldn't count as overlapping unless the problem says intervals are inclusive on both ends.
def max_concurrent_orders(intervals):
events = []
for start, end in intervals:
events.append((start, 1))
events.append((end, -1))
events.sort(key=lambda e: (e[0], e[1]))
count = peak = 0
for _, delta in events:
count += delta
peak = max(peak, count)
return peakThis is the same pattern as meeting-room scheduling, and interviewers here are checking whether you recognize it under a different word problem rather than pattern-matching only on the literal phrase "meeting rooms."
An inorder traversal of a BST visits nodes in sorted order, so the trick is rewiring the tree's own left and right pointers to act as prev and next during that traversal instead of building a separate list. Keep a reference to the previously visited node, and each time you visit a new node, point the previous node's right pointer at it and the new node's left pointer back at the previous one.
def tree_to_doubly_list(root):
prev = None
head = None
def inorder(node):
nonlocal prev, head
if not node:
return
inorder(node.left)
if prev:
prev.right = node
node.left = prev
else:
head = node
prev = node
inorder(node.right)
inorder(root)
return headThe in-place constraint is the actual test. Anyone can build a new linked list from a traversal; rewriting the tree's own pointers as you go and keeping the recursion correct is what separates a working answer from an easy one.
Reverse each group of k nodes the same way you'd reverse an entire list, then reconnect the tail of one reversed group to the head of the next. The part people get wrong under pressure is handling a final group with fewer than k nodes, most versions of this problem leave it unreversed, so you need to count ahead before committing to reverse a group.
def reverse_k_group(head, k):
node = head
count = 0
while node and count < k:
node = node.next
count += 1
if count < k:
return head
prev = None
curr = head
for _ in range(k):
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
head.next = reverse_k_group(curr, k)
return prevThe lookahead count before reversing is what makes this correct instead of accidentally reversing a partial group. It's a small check that's easy to skip when you're rushing.
Three cases: the node is a leaf, just remove it. The node has one child, splice the child up in its place. The node has two children, replace its value with its inorder successor (the smallest value in its right subtree), then delete that successor node, which by definition has at most one child.
def delete_node(root, key):
if not root:
return None
if key < root.val:
root.left = delete_node(root.left, key)
elif key > root.val:
root.right = delete_node(root.right, key)
else:
if not root.left:
return root.right
if not root.right:
return root.left
successor = root.right
while successor.left:
successor = successor.left
root.val = successor.val
root.right = delete_node(root.right, successor.val)
return rootCandidates who reach for the inorder predecessor instead of the successor aren't wrong, either works, but interviewers usually want you to pick one and justify it rather than waffle between both.
Track two running values at each position, the maximum product ending here and the minimum product ending here, because a negative number can flip the smallest product into the largest one on the next step. Reset both trackers to 1 whenever you cross a zero, since a zero breaks any subarray that would otherwise span across it.
def max_product_subarray(nums):
result = max_so_far = min_so_far = nums[0]
for n in nums[1:]:
if n < 0:
max_so_far, min_so_far = min_so_far, max_so_far
max_so_far = max(n, max_so_far * n)
min_so_far = min(n, min_so_far * n)
result = max(result, max_so_far)
return resultInterviewers here specifically want you to walk through a test case with a negative number followed by another negative number, since that's the exact scenario where forgetting the min tracker produces a wrong answer that still looks plausible.
Find the middle of the list with a fast and slow pointer, reverse the second half in place, then walk two pointers inward from both ends comparing values. Optionally reverse the second half back afterward if the list needs to be left unmodified.
def is_palindrome(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
prev = None
while slow:
nxt = slow.next
slow.next = prev
prev = slow
slow = nxt
left, right = head, prev
while right:
if left.val != right.val:
return False
left = left.next
right = right.next
return TrueCopying into an array and checking it's a palindrome is O(n) space and gets the answer, but the follow-up question is almost always "can you do this without the extra array," which is really asking whether you know the reverse-half technique at all.
Floyd's cycle detection: a slow pointer moving one step and a fast pointer moving two steps will eventually meet if there's a cycle. Once they meet, reset one pointer to the head and move both one step at a time, they'll meet again exactly at the start of the cycle, a fact that follows from the distance math between the head, the meeting point, and the cycle start.
def detect_cycle_start(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None
ptr = head
while ptr != slow:
ptr = ptr.next
slow = slow.next
return ptrMost candidates can detect the cycle. Fewer can explain, on request, why resetting one pointer to head and walking both at the same speed lands exactly on the cycle's start rather than somewhere else in the loop.
This is a set-difference problem, and the cleanest way to express it in SQL is with a NOT EXISTS or a LEFT JOIN with a null check, rather than pulling both months into application code and diffing them there.
SELECT DISTINCT o1.customer_id
FROM orders o1
WHERE o1.order_date >= '2026-01-01' AND o1.order_date < '2026-02-01'
AND NOT EXISTS (
SELECT 1 FROM orders o2
WHERE o2.customer_id = o1.customer_id
AND o2.order_date >= '2026-02-01' AND o2.order_date < '2026-03-01'
);NOT IN with a subquery looks equivalent but silently returns zero rows if that subquery ever produces a single null customer_id, which is a real footgun worth mentioning if the interviewer asks you to compare approaches.
A window function with a frame clause is the direct way to do this, avoiding a self-join or a correlated subquery that recomputes the average from scratch for every row.
SELECT city, order_date,
AVG(order_count) OVER (
PARTITION BY city
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS rolling_7d_avg
FROM daily_city_orders
ORDER BY city, order_date;The PARTITION BY keeps the rolling window scoped to each city independently, which is the detail people forget when they've only practiced window functions on single-series examples.
Order data has strong relational structure, an order belongs to a customer, references a restaurant, contains line items tied to a menu, and needs transactional guarantees so a payment and an order status update either both succeed or both fail. That points toward a relational database.
A NoSQL store earns its place elsewhere in the same system: caching a restaurant's live menu and availability for fast reads, storing delivery partner location pings at high write volume where eventual consistency is acceptable, or holding search indices for restaurant discovery. The honest answer isn't "pick one," it's that a system at this scale typically runs both, relational for the transactional core, NoSQL for the high-volume, loosely structured edges.
DENSE_RANK handles ties the way most people actually mean when they say "second highest," where two orders tied for first still leave the next distinct value as second. RANK would skip a rank number after a tie, which changes the answer depending on which one the interviewer expects.
SELECT restaurant_id, order_value
FROM (
SELECT restaurant_id, order_value,
DENSE_RANK() OVER (PARTITION BY restaurant_id ORDER BY order_value DESC) AS rnk
FROM orders
) ranked
WHERE rnk = 2;Naming which ranking function you picked and why is usually more important to the interviewer than the query itself, since it shows you've thought about what "second highest" means when values repeat.
Most indexes are a B-tree structure that lets the database jump to matching rows instead of scanning the whole table, at the cost of extra storage and extra write work to keep the index in sync with every insert, update, and delete on the indexed column.
An index makes things slower when write volume dominates and the index barely gets used for reads, every write now pays to update both the table and every index on it, or when the optimizer decides scanning the whole table sequentially is actually cheaper than jumping around via the index, which happens on small tables or queries that return a large fraction of the rows anyway. Adding an index isn't free, it's a bet that read savings outweigh write overhead, and that bet doesn't always pay off.
The restaurant's device sends an availability-update event the moment an item is marked sold out, which should propagate through a queue to whatever's serving menu data to customers, rather than customers reading menu data straight from a database the restaurant's app also writes to directly. Caching the menu is almost mandatory for read performance at scale, but that cache needs a short TTL or, better, an active invalidation on the specific item the moment the update event lands, otherwise customers keep ordering something that's already gone.
The real failure mode worth naming here is the race between a customer placing an order and the sold-out update landing. Even with fast propagation, there's a window where the order can still go through for an item that just ran out. The order-confirmation step needs a final availability check against the restaurant's live state before confirming, not just trust in whatever the cached menu said a few seconds ago.
Start from population, not from a guessed order count. Take a city of roughly 3 million people, estimate what fraction is in the addressable urban core with delivery coverage, maybe 60 percent, then estimate what fraction of those actually order food delivery in a given week versus never using the app at all, then divide down to a daily figure and apply a rough frequency, once or twice a week for an active user.
The number itself matters far less than whether you state each assumption out loud, sanity-check it against something you'd actually expect, would that many orders per restaurant per day be physically plausible for an average restaurant's kitchen capacity, and adjust when a check fails instead of defending the first number you landed on. Interviewers are watching whether you catch your own bad assumption before they have to point it out.
Break the average into its components before guessing at a cause: has kitchen prep time increased, has partner assignment slowed down, has actual travel time gone up, or is it a mix. Each of those points to a different root cause and a different owner, a spike in order volume overwhelming kitchens is a very different problem from a shortage of available delivery partners in a specific area.
Segment by area and time of day next, since a citywide average can hide a problem that's actually concentrated in two neighborhoods during dinner rush, and averaging it across the whole city and full day would dilute the signal to the point of looking like noise. Only after isolating where and when the regression is concentrated does it make sense to propose a fix, more partners in that zone, a change to the kitchen-side prep flow, or something upstream like new restaurants onboarded without enough delivery capacity around them.
First separate whether this is a platform problem or a market dynamic the platform shouldn't be fixing directly. If the drop is driven by genuine price competition from another restaurant, that's the market working as intended, and the platform's job isn't to protect one restaurant's order volume from a competitor's better offer.
Where it becomes worth investigating is if the drop is disproportionate to what price alone would explain, maybe the restaurant lost search ranking or visibility for reasons unrelated to price, a data issue, an algorithm change, a rating dip from unrelated causes. The instinct to check is whether something on the platform side changed for this restaurant specifically, not whether the restaurant's complaint feels sympathetic.
Absolute impact usually wins over percentage framing, one percent of orders in a small city might be a genuinely small number of affected customers, while a marginal performance improvement across the entire national user base compounds across a much larger volume even if each individual improvement looks minor.
The exception is severity. If that one percent bug means orders are failing outright, payment taken but food never arriving, that's a trust and retention problem that outweighs a raw impact calculation, since a customer who has one badly broken order is disproportionately likely to stop using the app entirely, not just skip one order. The right answer weighs both reach and severity together rather than defaulting to whichever number sounds bigger.
The useful version of this story shows you engaging with the disagreement on its technical merits rather than either caving immediately or digging in. Name the specific point of disagreement, what evidence or reasoning moved the conversation, and be honest about whose approach actually won, it's fine if it was theirs and you were wrong.
What interviewers are actually screening for is whether disagreement, on your team, historically got resolved through discussion or through whoever was louder or more senior winning by default. A story where you changed your mind based on a good argument is a stronger answer than one where you simply won every time.
The bar here is a gap nobody else was going to close, not just doing your assigned tickets well. Name what would have gone wrong if you hadn't stepped in, and be specific about the actual work, not just the intention to help.
Avoid the version of this answer where you took on extra work that didn't need doing, since that reads as a lack of judgment about priorities rather than ownership. The strongest answers show you noticed something that mattered, decided it was worth your time even though it wasn't formally assigned, and followed it through to a real outcome.
This is really a question about how you handle ambiguity under time pressure, not about whether you hit the deadline. Talk about the specific assumptions you made explicit before starting, who you checked them with if anyone, and how you'd have adjusted if one of those assumptions had turned out wrong.
A weak answer here glosses over the ambiguity entirely, as if the requirements were actually clear the whole time. A stronger one names a specific unresolved question you had to make a call on, and is honest about the risk of getting that call wrong.
Being honest here, without disclosing confidential details of another offer, usually works in your favor rather than against you. It gives the recruiter a real timeline to work with internally, and pretending you have no other process running when you do tends to surface as inconsistent later if timelines don't line up.
Hard questions
8This is Dijkstra's algorithm, and the version that trips people up under time pressure is implementing it correctly with a priority queue instead of the textbook adjacency-matrix version. Maintain a distance array initialized to infinity except the source, and a min-heap keyed by current known distance. Pop the closest unvisited node, relax its edges, and repeat.
import heapq
def shortest_paths(graph, source):
dist = {node: float('inf') for node in graph}
dist[source] = 0
pq = [(0, source)]
while pq:
d, node = heapq.heappop(pq)
if d > dist[node]:
continue
for neighbor, weight in graph[node]:
nd = d + weight
if nd < dist[neighbor]:
dist[neighbor] = nd
heapq.heappush(pq, (nd, neighbor))
return distThe stale-entry check, skipping a popped pair if its distance is worse than what's already recorded, is what keeps this correct when a node gets pushed onto the heap more than once. Miss that and the algorithm still often produces the right answer on small test cases, then breaks silently on the larger hidden ones.
The standard approach is a 2D DP table where dp[i][j] is the LCS length of the first i characters of one string and the first j of the other, filled by comparing characters and taking the max of skipping either character or matching both. That's O(n*m) time and space.
def lcs_length(a, b):
n, m = len(a), len(b)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
if a[i-1] == b[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[n][m]For the memory follow-up, you only ever need the previous row of the DP table to compute the next one, so rolling two 1D arrays instead of the full 2D table cuts space to O(min(n, m)) without changing the time complexity. That's the answer that separates someone who memorized the LCS table from someone who understands why it's shaped the way it is.
Sort tasks by deadline, then greedily try to schedule each task as early as possible, using a min-heap of already-scheduled task durations. If adding the current task would push the schedule past its deadline, check whether it's shorter than the longest task already scheduled, and swap it in if so, since a shorter task frees up more room for whatever comes later.
import heapq
def max_tasks_by_deadline(tasks):
tasks.sort(key=lambda t: t[1]) # sort by deadline
heap = []
total_time = 0
for duration, deadline in tasks:
heapq.heappush(heap, -duration)
total_time += duration
if total_time > deadline:
longest = -heapq.heappop(heap)
total_time -= longest
return len(heap)This is a variant of the classic job-scheduling-to-maximize-count problem, and interviewers here specifically map it to delivery slotting because it mirrors a real constraint the ops side of the business deals with daily.
An API gateway takes the incoming order request and routes it to an order service, which writes the order to a database and immediately publishes an order-created event to a message queue rather than trying to do everything inline in one request. Kafka fits well here because you genuinely need multiple independent consumers reacting to the same event: one consumer notifies the restaurant's kitchen display system, another kicks off delivery partner assignment, another updates analytics.
Delivery partner assignment itself is its own service, matching the order's pickup location against currently available partners nearby, factoring in who's already mid-delivery. Service discovery matters here because the partner-matching service, the restaurant-notification service, and the order service all need to find each other's current healthy instances without hardcoded addresses, especially since any one of them can scale independently based on load. If the order write succeeds but the queue publish fails, you need an outbox pattern or equivalent so the event isn't silently lost, that failure mode is usually the first thing a strong candidate flags unprompted.
Shard by a key that keeps related data together and matches how the data is actually queried, customer ID or restaurant ID are the natural candidates for an orders table, not something arbitrary like order ID hashed randomly, which would scatter a single customer's order history across every shard and make "show me my past orders" an expensive fan-out query.
Sharding by restaurant ID has an uneven-load problem: a handful of very high-volume restaurants would overload their specific shard while others sit mostly idle. Sharding by customer ID spreads load more evenly since no single customer generates a meaningful fraction of total order volume, at the cost of making restaurant-level analytics queries need to fan out across shards instead. Neither choice is free, and a candidate who names that trade-off explicitly, rather than picking a key and moving on, is usually the one the interviewer remembers.
An ETA at order time is really three estimates chained together: how long the kitchen needs to prepare the order, how long it takes a partner to reach the restaurant, and how long the delivery itself takes given current traffic and distance. Each of those has historical data behind it, average prep time per restaurant and dish, average partner travel speed by area and time of day, which feeds a model rather than a fixed formula.
Once the order is placed, that estimate needs to update as real signals come in: the kitchen marks the order ready earlier or later than predicted, the assigned partner's live location shows they're ahead of or behind the expected pace. That means the ETA isn't computed once and left alone, it's recalculated on a schedule or triggered by specific events, and the display needs to handle an ETA moving both earlier and later without feeling erratic to the person watching it.
The core question is whether waived delivery fees change ordering behavior enough to make up for the fee revenue given up, not whether the subscription itself sounds appealing. Model it from cohorts you'd expect to behave differently: a heavy user who already orders frequently gains little new incentive to order more, while an occasional user might genuinely order more often once the marginal cost of an order drops.
The riskiest assumption is usually adverse selection, the customers most likely to subscribe are the ones who already order the most, meaning the plan mostly discounts fees you were already collecting rather than driving new order volume. Testing that with a limited cohort before a full launch, tracking whether subscribers' order frequency actually increases versus their pre-subscription baseline, tells you more than any spreadsheet model built purely on assumptions.
The value in this story is entirely in the second half, not the mistake itself. Every engineer has made a wrong call at some point, what interviewers are actually assessing is whether you noticed on your own or needed someone else to point it out, how quickly you moved once you knew, and whether you changed anything about your process afterward or just moved on.
Avoid picking a mistake so minor it doesn't actually demonstrate anything, or one so severe you can't discuss it without sounding defensive. The sweet spot is a real decision with real consequences where your response to being wrong is the most interesting part of the story.
Real-time scenario questions
5Start with the data you need: a mapping of restaurants to users who've favorited them, and the restaurant's current open/closed state. When a restaurant flips to open, that's an event, publish it to a message queue rather than triggering notification logic synchronously inside whatever service updates the restaurant's status.
A consumer service picks up that event, looks up the interested users, and for each one checks a second condition before sending anything: is there actually a delivery partner available in that user's area right now. Notifying someone about a reopened restaurant that has no one to deliver from it is a worse experience than not notifying them at all. That availability check is usually a fast lookup against a partner-location service that's already tracking live positions, not something you compute from scratch per notification. Fan-out at this scale needs the send step itself batched and rate-limited per user, since a popular restaurant reopening could otherwise mean firing thousands of pushes in the same second.
A sliding-window or token-bucket counter per client key, API key, user ID, or IP, incremented on each request and checked against a threshold before the request is allowed through. The algorithm choice matters less than where the counter state lives.
INCR request_count:{client_id}:{window}
EXPIRE request_count:{client_id}:{window} window_seconds
if request_count > limit: rejectIn-process memory only sees requests hitting that one gateway instance. The moment you run more than one instance behind a load balancer, which any production API gateway does, each instance enforces its own separate limit and the real aggregate limit becomes instance_count times higher than intended. Redis gives every gateway instance a shared, atomic counter, so INCR plus a TTL enforces one consistent limit regardless of which instance handled the request. That's the actual reason Redis comes up specifically in this question rather than any generic in-memory cache.
Text search and geo-filtering are two different problems that need to combine, not one problem. A search index, Elasticsearch or similar, handles the fuzzy text matching on restaurant names, cuisines, and dish names, ranked by relevance and popularity signals. Geo-filtering narrows that result set to restaurants whose delivery radius actually covers the user's location, which is a separate geospatial query, often against a geohash or R-tree style index rather than the text search engine itself.
The two get combined either by post-filtering the text search results against the geo constraint, workable when result sets are small, or by encoding a geo bucket directly into the search index as a filterable field so both constraints apply in one query pass. At Zomato's scale, encoding location as an indexed field alongside the text data is the more common approach, since post-filtering a huge candidate set for every search gets expensive fast.
The partner's app sends periodic location pings, every few seconds, to a location service, which needs to handle very high write volume across every active delivery simultaneously. That location service writes to a fast key-value store keyed by delivery ID or partner ID, not a relational database, since you only ever need the latest position, not a full history for this particular use case.
The customer's app doesn't poll that store directly on a loop. A push mechanism, WebSocket connection or long-lived streaming connection tied to that specific delivery, forwards new positions to the customer's device as they arrive, so the map updates in near real time without the client hammering an endpoint every second. Once the delivery completes, that connection tears down and the live location data can be discarded or archived at much lower priority than it was tracked at.
Keep an orders table with a status column and a cancelled_at timestamp, plus a separate lightweight cancellations table if you need reason codes or who initiated the cancellation, customer, restaurant, or system timeout, since that's a one-to-one relationship that doesn't belong bloating every order row.
For a fast city-by-hour cancellation rate, don't compute it live over raw order rows at query time once volume gets large. Pre-aggregate into an hourly rollup table, city, hour bucket, total orders, cancelled orders, refreshed on a schedule or via a trigger-fed pipeline, and query that rollup instead. The underlying orders table stays the source of truth, the rollup exists purely so the dashboard doesn't scan millions of rows on every page load.
How to prepare for a Zomato interview
Practice the DSA topics that show up repeatedly in candidate reports rather than a random problem set: trees and BST manipulation, linked list reversal and cycle detection, and dynamic programming problems framed around subarrays or scheduling. Get comfortable writing SQL under time pressure, not just reading it, joins, window functions, and NOT EXISTS versus NOT IN are worth drilling specifically since both come up in reported rounds. For system design, study the domain itself, not generic distributed systems trivia. Knowing how a rate limiter, a notification fan-out, and a sharding strategy would actually apply to a food delivery platform matters more here than reciting CAP theorem definitions.
For product and business-facing rounds, the case studies aren't really about the final number, they're about whether you state assumptions out loud and correct course when a sanity check fails. Practice narrating your reasoning, not just landing on an answer.
Get the reps in before the real thing
Reading through fifty questions is not the same as defending an answer out loud when an interviewer changes one constraint and asks what breaks. LastRoundAI's mock interview mode runs live coding and system design rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused.
Once your answers hold up under a follow-up, the slower part of the job hunt is usually just getting in front of enough roles that actually test the skills you've been practicing. Auto-Apply queues tailored applications for your review, and nothing goes out until you approve it.
Questions about either product go to contact@lastroundai.com.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
How long is the Zomato hiring process?
Often three to eight weeks end to end, with the gap between onsite and decision being the slowest part. Team matching, where it applies, can add further time and is not a reflection on your performance.
How many interview rounds does Zomato have?
Usually a recruiter screen, a technical phone screen, then an onsite loop of four to five rounds covering coding, system design and behavioural. The exact count shifts by level and team, and some loops add a domain-specific round, so ask your recruiter for the actual schedule.
How hard is the Zomato interview?
Hard, but the difficulty is more about depth of follow-up than exotic questions. Interviewers tend to take a reasonable problem and keep pushing on trade-offs, edge cases and what you would change at ten times the load. Preparing to be interrogated on an answer matters more than memorising more answers.
What coding level should I prepare for a Zomato interview?
Comfortable medium-difficulty problems solved cleanly while talking, rather than hard problems solved silently. Most rejections come from unclear communication and untested edge cases, not from failing to find an exotic algorithm.
Does Zomato ask system design at every level?
From mid-level upward, almost always, and it carries increasing weight as you go up. Junior loops may include a lighter version focused on structuring a feature rather than designing a distributed system.
- Glassdoor: Zomato Interview Questions
- GeeksforGeeks: Zomato Interview Experience for SDE-1 (On-Campus 2021)
- Placement Preparation: Zomato Interview Questions and Experience
- Exponent: Zomato System Design Interview Questions
- Team Blind: Zomato Interview Discussions
- Product Management Exercises: Zomato PM Interview Questions
