AniUI Academy

Worked Scenario: Design a Search Autocomplete (Google-shape)

A full worked answer to one of the most-asked frontend prompts — debounced requests, cancellation, server-driven ranking, keyboard navigation, and the honest limits of local caching.

14 min read

Autocomplete is one of those prompts that reveals whether a candidate has actually built one in production or only used one. The naive implementation is five minutes of code; the real implementation is a careful stack of debounce, cancellation, caching, keyboard handling, ranking contracts, and ARIA. Nearly every FAANG+ frontend loop asks a version of this.

Clarifying requirements first

Before proposing anything, the questions worth asking out loud: Is the ranked suggestion set computed on the server (query is sent, server returns scored results) or locally (full dataset shipped once, filtered client-side)? How large is the full set — a few hundred items (local works), tens of thousands (server-only)? Does the result set depend on the signed-in user (personalisation)? Mobile-first-class or desktop-first? Must it work offline? For this answer, assume: server-driven ranking (set is huge and personalised), desktop + mobile both first-class, offline is out of scope, dropdown shows top 8 suggestions.

Rendering strategy: pure CSR, mounted after the shell

Autocomplete is an interactive widget — not a document, not indexable. Pure CSR. The host page can SSR anything around it; the widget itself mounts client-side after the shell paints and after the input is in the viewport.

The request pipeline: debounce + cancel + match-on-response

  1. Step 1

  2. Step 2

  3. Step 3

  4. Step 4

  5. Step 5

The ordering trap is the one that catches most candidates. Debounce reduces request count but doesn't bound ordering: a slow network delivers earlier results after later ones. Cancellation + match-on-response is what makes the ordering honest.

Caching: bounded LRU with stale-while-revalidate

LRU cache

An in-memory map keyed by (query, context) with a size cap (~50 entries) and a TTL (~60s). Pure JS, no library needed.

Stale-while-revalidate

If a cached result exists, paint it immediately. If it's older than a short freshness window, fire the request anyway and replace on return.

Context invalidation

On sign-in / sign-out / explicit user switch, purge the cache. Personalised results from session A must not render for session B.

The honest cost: a stale result is briefly visible before the fresh one replaces it. For autocomplete this is a correct trade — perceived speed beats a bounded freshness window users notice less than a 400ms spinner.

Keyboard and ARIA: the combobox pattern

An autocomplete that fails a keyboard user is not shippable at any serious company. The right ARIA primitive is the W3C combobox pattern:

  • <input role="combobox" aria-expanded="…" aria-controls="suggestion-list" aria-activedescendant="…"> — input stays focused throughout.
  • <ul id="suggestion-list" role="listbox"> with <li role="option" id="…">.
  • ArrowDown opens the list and moves highlight down; ArrowUp moves highlight up; Enter commits the highlighted option; Escape closes.
  • aria-activedescendant points at the highlighted option's id, so screen reader users hear the suggestion change without focus leaving the input.

The common mistake is Tab-to-navigate: that treats the list like a sequence of form controls, which is wrong for a picker.

Rendering the list: no need for virtualization at 8 items

The top-N dropdown is explicitly bounded (8 or so items), so virtualization is unnecessary. If product requires a "see all 50 results" expanded view, the expanded surface is a different render — likely a dedicated results page or a modal — not an expansion of the autocomplete dropdown.

Ranking is a server concern — the client's contract

The client does not re-rank what the server sent. If the server orders results by (personalisation + popularity + recency), the client renders in received order. Attempting to re-rank client-side introduces an inconsistency between the "what you type returns the same thing in two tabs" assumption. The contract is: server owns the ranking, client renders faithfully.

What the client does contribute: query highlighting (bolding the matching substring in each suggestion), icon or image per result, and a type chip ('person', 'product', 'help article') if the server tags them.

Error states: silent is wrong, loud is wrong

A failed autocomplete request should neither throw a toast nor pretend the dropdown is empty. The correct state is a small inline message in the dropdown: "Can't reach search right now." The input stays usable; typing retriggers the pipeline on the next debounce boundary. Rate limiting specifically should degrade to a longer debounce automatically.

What's explicitly out of scope, and why

Not solved in this answer: the ranking algorithm itself (a backend/ML concern); offline search (requires a different architecture around a local index); voice input (its own pipeline). Calling these out is a strength.

What to remember

  • Debounce reduces request count. Cancellation + match-on-response is what makes ordering honest. Three layers, not one.
  • A bounded LRU with a short TTL and stale-while-revalidate gives repeat queries free, with staleness explicitly bounded.
  • The combobox ARIA pattern is the right primitive — input stays focused, aria-activedescendant tracks the highlight, ArrowUp/Down/Enter/Escape handle navigation.
  • Ranking is the server's. The client's contract is: render faithfully. Re-ranking locally breaks consistency.
  • Error states live inline in the dropdown, not as toast noise. Rate limits degrade to a longer debounce.

Check yourself

3 questions · pass 3/3 to unlock Worked Scenario: Design a Video Player (Netflix / YouTube)

up to 50
  1. 1.A naive autocomplete fires an XHR on every keystroke to /search?q=…. A user typing 'laptop' sends six requests in under a second, four of which arrive out of order and overwrite the correct 'laptop' results with earlier 'lapto' ones. What's the right fix, and why does debounce alone not solve it?

  2. 2.The team wants the autocomplete to feel instant on repeat queries ('iph' → 'iphone' → 'iph' again). What's the honest caching story?

  3. 3.A keyboard user tabs into the input and types. The dropdown appears. What are the three keyboard interactions a strong implementation handles, and what's the ARIA primitive?

3 left to answer

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.