Frontend system design ยท about 40 minutes
Autocomplete search box
Design the search box of a large online store. As the user types, the box shows up to ten suggestions from the server. It must feel fast on desktop and on a phone with a slow network, and it must work with a keyboard and a screen reader. Choosing a suggestion opens the results page for it. Other teams want to reuse the same component with a different data source.
Ask first
- What can a suggestion be: plain text, a product with an image, a category, or a mix?
- How many suggestions do we show, and after how many characters do we start to search?
- Do we show recent or popular searches before the user types anything?
- Which devices and browsers matter, and how slow can the network be?
- Does the server handle typing mistakes and ranking, or must the client do something?
- Is this one search box, or a generic component with its own data source and item layout?
The outline
Write your own outline for each part first, then open it to compare with the points a strong answer covers.
Requirements
- Functional: type, see up to ten suggestions, move with the arrow keys, choose with Enter, a click or a tap, close with Escape
- Speed: one request per typing pause, not per key; a query that was seen before shows at once from the cache
- Correctness: the list never shows results for an older query than the text in the input
- Access: full use with the keyboard only and with a screen reader; usable on a small touch screen
- States: loading, no results and a failed request, none of which block typing
- Out of scope unless asked: ranking on the server and the full results page
Architecture
- Three parts: the view (input and popup list), a controller that owns the query, the open state and the active item, and a cache between the controller and the server
- On each change the controller looks in the cache first; on a miss it waits for a typing pause (debounce, about 200 to 300 ms) and then calls the API
- Each request gets an AbortController signal, and a new query aborts the request before it
- The controller also drops any response whose query differs from the current input, in case the abort came too late
- The cache is in memory, keyed by the normalized query (trimmed, lower case), with a size limit and a time to live
- The data source is passed in as a function, so the same component can search products, users or a local list
Data model
- Controller state: query, status (idle, loading, success, error), items, activeIndex (-1 when none), isOpen
- Suggestion: id, label (the text shown and put into the input), type, and optional image URL and target URL
- Cache: a map from the normalized query to the items and the time stored; the least recently used entry is removed over the limit
- The highlighted option is derived from activeIndex; it is not stored a second time
- Recent searches, if needed: a short list of strings in localStorage
Interface
- Props: fetchSuggestions(query, signal), onSelect(item), renderItem, minChars, debounceMs, maxResults, label, placeholder
- API: GET /suggestions?q=shoe&limit=10, with the query URL-encoded; a GET, so the browser and a CDN can cache it
- Response: { query, items: [{ id, label, type, url }] }; the echoed query lets the client drop a stale response
- Markup contract: the input has role combobox, aria-expanded, aria-controls and aria-activedescendant; the popup has role listbox; each item has role option
- Events out: onSelect with the item, and onQueryChange for analytics
Optimizations
- Fewer requests: debounce, a minimum length, and the cache, which also makes Backspace instant
- HTTP caching: a Cache-Control header on the response lets the browser and a CDN reuse answers for popular queries
- Accessibility: DOM focus stays in the input, arrows move aria-activedescendant, and a polite live region says how many suggestions there are
- Errors: keep the last good list or show a short message with a retry; never clear what the user typed
- Safety: server text is rendered as text, and the matching part is highlighted with elements, not with innerHTML
- Edge cases: no search during IME composition, pasted text, very long input, and an empty input closes the list
- Mobile: large touch targets, a list that stays visible above the on-screen keyboard, and the browser's own autocomplete turned off
Pitfalls
- Showing the response to an older query because it arrived last
- Sending one request for every key press
- A list of div elements with click handlers only: no combobox roles and no keyboard support
- Moving DOM focus into the list, so the user cannot continue typing
- Putting text from the server into the page with innerHTML
Follow-ups an interviewer asks
- Responses arrive out of order on a slow network. What does the user see, and why?
- How large is your cache, and when is an entry too old to use?
- How does a screen reader user know that suggestions appeared?
- How would the design change for 10,000 local items and no server?
- Debounce delays the first result. How do you keep the box feeling fast?
- What do you measure in production to know that the box works well?