Frontend system design rounds rarely ask you to present a finished design. They ask for something small, let you build it, and then change the rules.

"Design a search box with suggestions" takes two minutes to answer naively. The next forty are the interviewer saying now what about… — and each round is checking whether you can find what broke and change one thing, rather than starting over or defending the first version.

So that's how this page runs: a deliberately simple version one, then five rounds of constraints, each with what broke, what changes, and what the change costs.

10→1
Requests for one word, before and after round one
5
Rounds of “now what about…” in a typical session
0
Places the suggestion text should touch innerHTML

Version one, in two minutes

"Design an autocomplete search box. As the user types, show up to ten suggestions from an API."

"Simplest version first, so we have something to change: an input, and on every change I request suggestions for the current text and render them as a list under the box. Clicking a suggestion fills the input. I know this has problems — I'd rather show you the baseline and fix it than design for problems we haven't agreed on."

Saying "I know this has problems" is not a weakness. It signals that the naivety is deliberate, and it hands the interviewer the next move.

Round one: "It's hammering the API"

Typing kubernetes at a normal pace is ten keystrokes, and version one sends ten requests — nine of them for words nobody wanted.

"Three changes. Debounce the input at around 250 milliseconds, so while someone is typing continuously nothing is sent until they pause — for that word, ten requests become one. Don't search below two characters, because one-letter results are noise. And cache results by query in a small bounded map, so deleting a letter and retyping it costs nothing."

The cost, said out loud: debouncing adds 250 milliseconds before anything appears, which some users will feel as lag. The difference between debouncing and throttling is a definition question you'll get elsewhere — it's question 13 in Frontend Developer Interview Questions. Here the interviewer wants the number and its trade-off, not the definition.

Round two: "Sometimes it shows results for what I typed earlier"

This is the round the whole exercise exists for.

"That's a race. I type 'react', pause, type 'react native'. Two requests go out. If the first one is slow and the second is fast, the second response arrives first and renders, then the first one arrives and overwrites it — and the user sees suggestions for 'react' under a box that says 'react native'.

Two fixes, and I want both. Cancel the previous request when a new one starts, with an AbortController — that saves bandwidth. But cancellation isn't a guarantee: a response can already be in flight or being parsed. So I also tag each response with the query it was for, and when it lands I ignore it unless that query is still what's in the box. The cancel is an optimisation; the check is the correctness."

"The cancel is an optimisation; the check is the correctness" is the sentence the round is graded on. Most candidates who spot the race fix it with cancellation alone, and that version still fails intermittently — which is the worst kind of frontend bug, because it never reproduces on the developer's fast laptop. Naming why one fix isn't enough shows you've debugged this in production rather than read about it.

Round three: "A screen reader user can't use it"

"Then it isn't finished. The input becomes a combobox: role="combobox", with aria-expanded showing whether the list is open and aria-controls pointing at the list. The list is role="listbox" and each suggestion is role="option".

Keyboard focus stays in the input the whole time — moving it into the list breaks typing. Arrow keys move a highlight, and the highlighted option is announced through aria-activedescendant. Enter selects, Escape closes and keeps what was typed. And a polite live region announces the count — 'seven suggestions' — so someone who can't see the list knows it appeared."

The cost is honest and worth saying: this is roughly a third of the component's code and the part most often skipped under deadline. Naming the pattern correctly matters less than knowing focus must stay in the input — that's the detail that tells an interviewer you've watched someone actually use one.

Round four: "Make it fast on a cheap phone"

"First, don't render what nobody will read: the API returns the top ten, and I never render a list of hundreds. If the product genuinely needed long lists, I'd virtualise, but for suggestions that's solving the wrong problem.

Second, keep the previous results visible while the next request is loading, instead of clearing to an empty state. Clearing on every keystroke makes the list flicker, and on a slow phone the flicker is most of what the user experiences.

Third, highlighting the matched part of each suggestion: I'd split the string and render the match as a separate element. Not by building an HTML string and injecting it — suggestion text comes from the server, and injecting it is a cross-site scripting hole wearing a styling decision."

That last point is the one that surprises interviewers, because it arrives in a performance round. Security bugs in frontend design are almost always introduced as presentation choices.

Round five: "What does the component's API look like?"

The closing round shifts from behaviour to design for reuse.

"The component shouldn't know where suggestions come from. I'd pass it a function that takes a query and returns suggestions, plus what to do on selection and how to render one item. Then it works against a search API, a local list, or a test fake — and the race-condition logic from round two is written once, inside the component, instead of being reimplemented by every caller.

I'd expose the minimum characters and the debounce delay as options with sensible defaults, and I'd resist adding more. Every prop is a promise to keep supporting it."

The whole round, as one table

RoundWhat brokeThe changeWhat it cost
1A request per keystrokeDebounce, minimum length, cache250 ms before results appear
2Stale results overwrite fresh onesCancel stale requests, and ignore responses for old queriesSlightly more state to manage
3Unusable without a mouse or sightCombobox pattern, focus stays in the input, live regionA third of the code
4Flicker and jank on slow devicesCap results, keep old results while loading, no HTML injectionHighlighting is more code
5Logic tied to one data sourceInjected fetch function, small option setFewer knobs for callers

Every row has a cost column, and that column is what the interviewer is actually scoring. A design where every change was free hasn't been examined.

What answers the wrong question

Framework trivia

“I'd write a custom hook with useEffect and useState, memoise the list with useMemo, and wrap the handler in useCallback.”

Accurate React, and it describes how to type the component rather than how it behaves. None of it addresses the stale-response race, which is the bug the interviewer planted.

What the round is for

Behaviour and failure

“Responses can arrive out of order. Cancelling helps, but the check against the current query is what makes it correct.”

Framework-independent, tied to a user-visible bug, and it names why the obvious fix is incomplete. That's the reasoning that transfers to the next component they'll ask about.

Saying it out loud

Narrate the version you're building before you improve it, and narrate each change as what broke, then what changes. The structure keeps you from redesigning everything when the interviewer adds a constraint — and it gives them a clean place to interrupt, which they will.

The phrase worth practising until it comes out cleanly is the race explanation — two queries, two responses, the slow one landing last. It's the hardest part of this round to say fluently, because it's a sequence of events, and sequences are where hesitation shows.

The server side of the suggestion endpoint — pagination, the error shape, what a public API can never change — is its own design round: the API design round. If the loop also includes a take-home, this component is a common one to be handed: the take-home coding assignment.

Practical target: build version one in under an hour against a fake API that answers each request after a random delay between 100 and 1,500 milliseconds. Type quickly. If the list ever shows suggestions for something other than what's in the box, you've reproduced round two yourself — and fixing it on your own machine is the best preparation there is for explaining it in the room.