System Designmedium9 min read

Design an Autocomplete (Typeahead)

The most common frontend system design prompt, end to end: debouncing, caching, race conditions, keyboard accessibility, and the trade-offs interviewers actually probe.

Published · by Frontend Masters India

"Design an autocomplete" sounds small. It isn't. It's the prompt interviewers reach for because a complete answer touches networking, caching, concurrency, accessibility, and product judgement, all in a component that fits on one screen.

Here's how to walk through it without leaving gaps.

1. Clarify the requirements first

Don't start drawing. Ask:

  • What are we searching? Search queries (Google), users (a mention box), products (an e-commerce bar)? This decides result shape and ranking.
  • How many results, and do we group them? A flat list of 10 is very different from grouped sections with headers.
  • Do results need to be keyboard- and screen-reader-accessible? The answer is always yes; saying it unprompted scores points.
  • Network constraints? Mobile, flaky connections, and rate limits change the caching and debounce story.

State your assumptions out loud, then design to them.

2. The component breakdown

<Autocomplete>
  <SearchInput />        // controlled input, ARIA combobox
  <ResultsList>         // listbox, virtualized if large
    <ResultItem />      // option, aria-selected
  </ResultsList>
</Autocomplete>

The container owns state: the query, the results, the loading flag, and the highlighted index. Children are presentational.

3. Don't fire a request per keystroke

Typing "react" is five keystrokes. Five requests, of which only the last matters. Two tools:

  • Debounce the input by ~200–300ms so you only query when the user pauses.
  • Throttle instead if you want periodic updates during fast typing. Rarely the right call for search.
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

4. The race condition everyone forgets

Even with debouncing, responses can arrive out of order. You type "re", then "react"; the "re" request is slow and resolves after "react", clobbering the correct results with stale ones.

Fix it by tracking the latest query and ignoring any response that doesn't match it, or by using AbortController to cancel in-flight requests:

const controller = new AbortController();
fetch(`/api/search?q=${q}`, { signal: controller.signal });
// before the next request:
controller.abort();

Mentioning this unprompted is one of the clearest signals of a senior frontend engineer.

5. Caching

A client-side cache keyed by query string avoids re-fetching results the user has already seen (think backspacing). A simple Map works; an LRU cache bounds memory. Decide whether stale results are acceptable and for how long.

6. Accessibility is not optional

Implement the ARIA combobox pattern: role="combobox" on the input, role="listbox" on results, aria-activedescendant pointing at the highlighted option, and full arrow-key / Enter / Escape handling. An autocomplete that only works with a mouse is incomplete.

7. What the interviewer will push on

  • "What if the list is huge?" Virtualize the rendered rows so the DOM stays small.
  • "How do you handle errors and empty states?" Show a distinct "no results", and never leave a spinner spinning forever.
  • "How would you test this?" Debounce timing, the race condition, and keyboard navigation are the high-value cases.

The one-paragraph summary

A great autocomplete debounces input, cancels or ignores stale responses, caches by query, virtualizes long lists, and is fully keyboard- and screen-reader-accessible. Lead with the race condition and accessibility. They're what separate a working demo from a production component.

Before you leave — how confident are you with this?

Your honest rating shapes when you'll see this again. No grades, no shame.

Comments

to join the discussion.

Loading comments…

More design prompts