FSD Frontend System Design

Part V β€” The Craft Β· Lesson 10.15

Autocomplete & Search Bar

In one lineDebounce, cancel, cache, and keyboard-navigate β€” the most-asked LLD question, and the most detail-rich.

Where you've seen itAny good search box. And the bad ones, where results briefly show matches for "piz" after you finished typing "pizza".

  • Time19 min
  • DiagramKeystroke timeline with debounce + race condition
  • Chapter10 Β· Low Level Design

Diagram

diagram
DEBOUNCE β€” one request instead of five

  keystrokes  p    i    z    z    a
              β”‚    β”‚    β”‚    β”‚    β”‚
  t(ms)       0   90  180  260  340        ────► 700ms (idle)
                                                    β”‚
  no debounce β–Ό    β–Ό    β–Ό    β–Ό    β–Ό                 5 requests
  300ms debounce ──────────────────────────────────►▼ 1 request
diagram
THE RACE β€” why the wrong results appear

  request "piz"   ────────────────────────────► 420ms ──► arrives 2nd
  request "pizza" ──────────► 180ms ──► arrives 1st

  timeline:  pizza results shown ──► piz results OVERWRITE them βœ—

  FIXES (use both)
    1. AbortController: cancel the previous request on each new one
    2. Ignore any response whose query β‰  the current input value
diagram
KEYBOARD + ARIA CONTRACT (8.2, 8.3)

  ↓ / ↑        move the active option (no focus change)
  Enter        select the active option
  Esc          close the list, keep the text
  Tab          close and move on

  <input role="combobox" aria-expanded="true"
         aria-controls="listbox-1"
         aria-activedescendant="opt-3">     ← focus STAYS in the input
  <ul id="listbox-1" role="listbox">
    <li id="opt-3" role="option" aria-selected="true">pizza near me</li>
diagram
STATE MACHINE

  [idle] ──type──► [debouncing] ──idle 300ms──► [loading]
                        β”‚                          β”‚
                        β”‚                     β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
                        β”‚                  results    empty
                        β”‚                     β”‚          β”‚
                        β–Ό                     β–Ό          β–Ό
                  [idle] ◄──Esc────────── [open]    [no results]

How it works

  1. Debounce ~300ms on the input, and set a minimum query length (2–3 characters) before firing anything.
  2. Cancel in-flight requests with AbortController, and additionally discard stale responses by comparing the query string.
  3. Cache by query (6.9) so backspacing to a previous term is instant and costs no request.
  4. Wire the full keyboard contract β€” arrows move aria-activedescendant while focus stays in the input.
  5. Handle every state: idle, debouncing, loading, results, empty, error, and offline. Empty-with-suggestions beats a blank box.
js
useEffect(() => {
  if (query.length < 2) return setResults([]);
  const controller = new AbortController();
  const timer = setTimeout(async () => {
    if (cache.has(query)) return setResults(cache.get(query));
    try {
      const data = await fetch(`/search?q=${encodeURIComponent(query)}`,
                               { signal: controller.signal }).then((r) => r.json());
      cache.set(query, data);
      setResults(data);
    } catch (e) { if (e.name !== 'AbortError') setError(e); }
  }, 300);
  return () => { clearTimeout(timer); controller.abort(); };   // both, always
}, [query]);

Trade-offs

DecisionConsequence
Debounce 300msFewer requests; slight perceived lag
Debounce 150msSnappier; roughly double the traffic
Throttle insteadSteady request rate; more total requests
Client-side filterInstant, only viable for small datasets
Cache per queryInstant backspace; memory to bound

Also worth mentioning: highlight the matched substring, sanitize before rendering it (3.2/3.9), and track the query for analytics β€” search terms with no results are the single best product signal you can collect.

Interview angle

"Build an autocomplete. What are the failure modes?"

  • The race: an earlier slow request landing after a later fast one β€” fix with AbortController and a stale-response check.
  • The traffic: no debounce and no minimum length means a request per keystroke.
  • The accessibility: it's a combobox with aria-activedescendant, so focus stays in the input while arrows move the selection β€” most implementations get this wrong.

Recap

  • Debounce, minimum length, abort, and discard stale responses β€” all four.
  • Cache by query so backspace is free.
  • It's a combobox: arrows move the active option, focus never leaves the input.