FrontendPro

137Design an autocomplete system serving millions of users with caching and request cancellation

hardAbout 60 minutesSystem designFreeautocompletecachingperformanceaccessibilitysystem-design

Problem

Design the search box of a large site (an e-commerce store or a search engine) that shows suggestions as the user types, for millions of users.

  • Suggestions appear within about 100 ms of typing.
  • Results are always for what is currently typed, never for an older query that came back late.
  • The client minimises requests and the backend copes with peak traffic.
  • It works well with keyboard, screen readers and on slow mobile networks.
  • Suggestions can mix types: recent searches, popular queries, products, categories.

Cover the component API, the request strategy (debounce, cancellation, caching), the client cache, rendering, accessibility, and how the frontend cooperates with the backend and CDN.

Video walkthroughs

Stuck? Watch someone solve it, then try again on your own.

Design a autocomplete widget / Typeahead widget | Frontend System Design Interview QuestionFrontend Jirachi on YouTubeOpen on YouTube
Frontend System Design Interview: How to Design a Typeahead Component (With This 5-Step Model)I Code It on YouTubeOpen on YouTube

Step by step

Build the solution one idea at a time.

  1. 1

    Clarify the product

    What is suggested (queries, products, mixed), how many results, whether personalised, and devices. This decides caching (personalised results cannot be shared at the CDN) and layout.

  2. 2

    Design the component contract

    A headless, reusable Autocomplete with fetchSuggestions(query, { signal }), renderItem, onSelect, and options for debounce, minimum length and cache. The UI layer follows the ARIA combobox pattern.

  3. 3

    Make each keystroke cheap

    Instant local results (recent searches, cached prefixes), a short debounce for the network, cancellation of stale requests, and a response-to-query check so late results never win.

  4. 4

    Cache at every layer

    In-memory LRU on the client keyed by normalised query, HTTP caching with CDN edge caching for popular non-personalised prefixes, and server-side precomputed prefix indexes.

  5. 5

    Measure and degrade gracefully

    Track latency and usage, and fall back to cached or local results when the network is slow or failing.

Requirements

Functional

  • Show up to 8–10 suggestions as the user types; highlight the matching part.
  • Keyboard: arrows to move, Enter to select, Escape to close, Tab to accept; mouse and touch selection.
  • Mixed groups: recent searches (local), popular queries, products with thumbnails.
  • Submitting the query (not a suggestion) goes to the results page.

Non-functional

  • p95 time from keystroke to suggestions under ~150 ms for cached prefixes, ~300 ms otherwise.
  • No more than one in-flight request per input; no stale results displayed.
  • Backend load bounded: aim for well under one request per keystroke on average.
  • WCAG-compliant combobox; works on low-end phones.

Architecture

┌──────────────────────── Browser ─────────────────────────┐
│ <SearchBox>                                               │
│  input (role=combobox) ─ onChange ─┐                      │
│                                    ▼                      │
│  Suggestion controller                                    │
│   1. normalise query ("  iPhone " → "iphone")             │
│   2. local results now: recent searches, LRU cache hit    │
│   3. debounce 100–150 ms                                   │
│   4. abort previous request, fetch with AbortSignal        │
│   5. accept response only if query === latestQuery          │
│   6. store in LRU, render                                   │
│  listbox (role=listbox) ─ options (role=option)            │
└──────────────────┬───────────────────────────────────────┘
                   ▼ GET /suggest?q=iph&locale=en-IN  (Cache-Control: public, max-age=300)
        CDN edge cache (popular prefixes) ──▶ Suggest service ──▶ prefix index (trie / FST)
  • Controller is framework-agnostic: it holds the latest query, the cache, the in-flight AbortController, and emits states (idle, loading, results, error).
  • Client cache: an LRU (for example 100 entries) keyed by normalised query. A longer query can also be answered locally by filtering a cached shorter prefix when the server marked that result as complete.
  • Server: a precomputed prefix index (trie or finite-state transducer) of popular queries with scores, so lookups are O(length of prefix); results for popular prefixes are cacheable at the CDN because they are not personalised. Personal suggestions (recent searches) are merged on the client.

Data model and state

type Suggestion =
  | { type: "query"; text: string }
  | { type: "recent"; text: string }
  | { type: "product"; id: string; title: string; imageUrl: string; price: string };

type SuggestState = {
  query: string;               // what the input shows
  latestRequested: string;     // the normalised query the UI waits for
  status: "idle" | "loading" | "results" | "error";
  items: Suggestion[];
  activeIndex: number;         // keyboard highlight, -1 for none
};

type Cache = LRU<string /* normalised query */, { items: Suggestion[]; complete: boolean; fetchedAt: number }>;

Recent searches live in localStorage (capped, de-duplicated, removable by the user); they are shown immediately on focus with an empty input.

Key flows and trade-offs

Typing "iph"

  1. i: show recent searches matching "i" instantly; start the debounce timer.
  2. ip: restart the timer.
  3. iph: the timer fires 120 ms after the last key; cache miss, so abort nothing (none in flight) and send GET /suggest?q=iph.
  4. The user types o while it is in flight: abort it (saving bandwidth), start the timer for "ipho".
  5. A response is applied only if its query equals latestRequested; otherwise it is cached but not shown.

Backspacing to "iph" hits the LRU and renders instantly with no request.

Request reduction math: with a 120 ms debounce, typical typists (5–8 keys/s) trigger requests mostly on pauses; client caching removes repeats; CDN caching absorbs the head of the distribution (a few thousand popular prefixes cover most traffic).

Trade-offs

Decision Choice Why
Debounce ~100–150 ms Short enough to feel instant, cuts requests a lot
Throttle instead? No, debounce + cache Throttle sends intermediate prefixes users skip
Stale responses Abort + query check Abort saves bandwidth; check guarantees correctness
Personalisation Client-merged recents, server popular Popular results cacheable at the edge
Rendering Max ~10 items, no virtualisation Small lists; keep DOM simple

Performance and reliability

  • Fast first suggestion: preconnect to the suggest endpoint on focus; prefetch suggestions for the empty query (trending) on focus.
  • Payload size: short JSON with only displayed fields; compressed; thumbnails small and lazy.
  • Main thread: keep rendering cheap; highlight matches with string slicing, not innerHTML; avoid layout thrash in the dropdown.
  • Slow networks: show local results immediately, a subtle loading indicator after ~300 ms, and keep showing the last results while new ones load to avoid flicker.
  • Failures: on errors, keep local and cached suggestions; never block typing or form submission.
  • Backend protection: minimum query length (1–2 characters), server-side rate limits per user, and CDN caching; the client respects Retry-After.
  • Metrics: keystroke-to-render latency, requests per search session, cache hit rate, suggestion click-through rate, and zero-result rate.

Follow-up questions

Answer each one out loud first, then reveal the model answer.