Design prompt: a typeahead search box
Put numbers on requests, latency and bytes first; every cache and reuse rule must leave each list exactly what the service would send.
Predict first
Before you read: predict
ExamHall's lobby search box asks the session service GET /suggest?q= once the candidate pauses. The service answers with the first 8 matching sessions among its 30,000 sessions, in date order, and the total it found; the session list changes only overnight. The box keeps every answer for the page's life. She copies her session code from her letter: she types econ-2, pauses, types 01-ber, pauses, types -06, pauses, types 134, pauses, then deletes 34 and pauses.
Which reuse rule sends the fewest requests while every list she sees is the one the service would send for her text?
The 30-second version
A typeahead's design is its budgets. A small list ships with the page; otherwise one suggest endpoint. Requests come from pauses on new text; the debounce counts toward the latency target. Keep answers under the text folded exactly as the service folds it. A kept answer serves a longer text only if it held every match.
Put numbers on requests, latency and bytes first; every cache and reuse rule must leave each list exactly what the service would send.
How it works
Step 1 of 11
Analogy: A map shop's place-name desk
A customer spells the town she wants a map of; the clerk phones the national gazetteer office to find it.
| At the desk | In the design |
|---|---|
| for the home county, the clerk looks the village up in the index booklet on the counter | a small list shipped with the page (step 2) |
| she phones only when the customer stops spelling | one request per pause (step 4) |
| answer slips are filed as the gazetteer spells its index: no capitals, no accents, hyphens kept | the key folds as the service does, no further (steps 7–8) |
| a slip reading "all 3 places" answers a longer name by crossing lines out; "first 8 of 41" cannot | reuse only complete answers (step 9) |
| the office reads out where the name sits in each entry | positions from the service (step 10) |
In real terms: a typeahead is a budgeted stream of requests, one per pause, whose answers are kept under the service's own spelling of each text.
Breaks down when: a gazetteer barely changes in a lifetime; a session list changes overnight, so kept answers live only as long as the page.
Contrasting case: choosing one of six regions from a fixed list. That is a select: nothing to search, fold or budget.
Examples, run for real
4.1 ExamHall EXH-2910 → MR !3210: the lobby box at the exam-start peak
The lobby's pilot box asked the session service at every keystroke and kept nothing. On the morning of a big sitting it sent more requests than the service's limit. MR !3210 waits 250 ms after the last key and keeps each answer under the folded text (steps 4, 5, 7). The typing is a seeded model of 400 lobby searches (lib/corpus.mjs).
4.1 model: 400 lobby searches · keys per search 18.6 · peak 300 searches a second · service limit 2,000 requests a second
4.1 EXH-2910 pilot, a request per key, nothing kept: 18.6 requests per search · 5,580 a second, over the limit
4.1 MR !3210, 250 ms debounce, answers kept under the service's fold: 5.1 requests per search · 1,537 a second (kept under the text as typed: 5.6)
4.1 the whole list, 30,000 sessions as { id, code, title, centre }: 2,906 KB of JSON, 365 KB gzipped · lobby data budget 150 KB: does not fit
The keystrokes, not the searches, drove the pilot's load (steps 4–5). The whole session list is too big to ship with the page, so the lobby keeps an endpoint (steps 2–3).
4.2 Media streaming: the suggest contract
A streaming service's search box. The viewer types amelie at, with no accent; the service folds case and accents, so it matches "Amélie at Sea".
4.2 GET /suggest?q=amelie%20at -> {"q":"amelie at","total":24,"items":[{"id":"T0017","title":"Amélie at Sea · S3 E3","ranges":[[0,6],[7,9]]},{"id":"T0035","title":"Amélie at Sea · S1 E3","ranges":[[0,6],[7,9]]}]} (first 2 of 8 shown)
4.2 positions in "Amélie at Sea · S3 E3": [0, 6] = "Amélie", [7, 9] = "at" · items 8 of total 24: cut
The answer carries the text it answered, the total that match and, per title, where each typed word sits in the title as stored (step 3). With 24 matches and 8 shown, it is cut, so a longer text needs its own request (step 9).
4.3 The board for "Design a typeahead" (worked, not executed)
| Typical question | Steps that answer it | Mechanism owner |
|---|---|---|
| "500,000 daily searchers: how many requests a second?" | 1, 4, 5 | this lesson; the peak factor: sd-back-of-envelope |
| "What do you cache, and under which key?" | 7–9 | this lesson; a query cache's mechanics: fsd-server-state-caching |
| "How do you highlight, and handle diacritics and RTL?" | 7, 10 | this lesson; dir="auto": ui-logical-properties-rtl |
| "Walk me through the keyboard and screen reader." | — | uibuild-typeahead |
Try it
Answer first, then check. Nothing is saved.
Question 1 of 7
A streaming service sizes its search box for the evening peak, when 30 searches start each second. The box waits 300 ms after the last key before it asks GET /suggest?q=; the service matches titles without regard to case or accents; the box keeps every answer for the page's life and sends no request it can do without. A typical peak search, from the logs: the viewer types cre, pauses, types me, pauses, deletes me, pauses, then selects the box and types Crème, pauses. Every text she pauses on matches more than 8 titles, and each key comes well inside the 300 ms.
How many suggest requests a second should the board's estimate say?
Question 2 of 7
The lobby marks the typed word inside each suggested title with this draft, where fold removes accents and vowel marks and lower-cases, as the session service does when it matches:
Titles are shown as each centre stores them. Candidates search micro, mikrooko and الكلي ("macro", typed without vowel marks), and the service suggests English, German and Arabic titles, some of the Arabic ones stored with their vowel marks. On one Arabic title the mark covers the wrong letters.
Which change marks exactly the typed word in every title, with every title shown as stored?
function mark(title, text) { const at = fold(title).indexOf(fold(text)); if (at < 0) return [title, '', '']; return [title.slice(0, at), title.slice(at, at + text.length), title.slice(at + text.length)];}Question 3 of 7
A payments console's merchant search asks the merchant service at each pause, with a 150 ms debounce, and must show suggestions within 450 ms of the operator's last key for 95% of searches. The service and the round trip together take 140 ms at the median and 260 ms at the 95th percentile, and drawing the list takes one 16 ms frame. At today's peak the console sends 200 requests a second. The interviewer's next follow-up: "Ten times the operators. At the peak the console now sends 2,000 requests a second, and the merchant service takes 1,200 at most. The latency target stays." The logs say that 55% of requests are for texts of 1 or 2 characters, that no merchant needs fewer than 3 to be found, that a 250 ms debounce would send 60% as many requests, and that 12% of requests repeat a text, folded as the service folds it, that the same operator already asked on that page, while 45% of searches hold such a repeat.
Which one change answers the follow-up?
Question 4 of 7
A concert hall's site lets fans search its 2,400 upcoming events by title or by venue from the home page, which may add at most 100 KB to what the page downloads. As JSON, an event's id, title, venue and date take about 90 bytes, and its id and title alone about 40; the site's compression shrinks such lists at least 4 to 1. The events service answers GET /suggest?q=.
Which design sends the fewest requests to the events service, stays within the 100 KB, and finds every event by its title or its venue?
Question 5 of 7
The interviewer says: "Design the frontend of ExamHall's candidate lobby search: 30,000 sessions by code, title and centre, in English, German and Arabic." In 20 minutes, write your board: the numbers you would ask for, the API, what the client keeps and under which key, when a kept answer may serve another text, one estimate, and the follow-up you expect.
Then write your design in the shape of design.template.mjs as my-design.mjs, and run the check.
Question 6 of 7
The lobby's draft caches replies under a cleaned text, where fold removes accents and lower-cases; the session service matches without regard to case or accents:
Candidate 1 types econ 201, pauses, and sees that no session matches. She changes the space to a hyphen, so the box reads econ-201, and pauses: still no session, though the service lists 3,002 sessions for econ-201. Then she selects the box, types ECON-201 from her letter and pauses. On another page, candidate 2 types münchen, pauses, then selects the box, types munchen and pauses.
Which one change makes every list the service's own, with no more requests than that needs?
const cache = new Map();const clean = (text) => fold(text).replace(/[^\p{L}\p{N}]+/gu, ' ').trim();async function suggestions(text) { const key = clean(text); if (!cache.has(key)) cache.set(key, await getJson(`/suggest?q=${encodeURIComponent(text)}`)); return cache.get(key).items;}Question 7 of 7
A chat app's people search must show suggestions within 400 ms of the user's last key for 95% of searches. At the 95th percentile the people service answers in 70 ms. The network round trip takes 50 ms at the median and 140 ms at the 95th percentile, and the list draws in one 16 ms frame.
Which is the longest of these debounces that meets the target?
Go deeper
Pro tip
- Put a number on the debounce saving before drawing boxes: keys per search against requests per search, then times the peak searches. When: the requirements phase of a typeahead round (the phases are
fsd-radio-framework's). Status: RAN on the seeded corpus (a model of typing, not logs).
Source
No item is keyed on a source; every key is executed. A guide's interview advice is valid as of 2026-10-04.
- Front End Interview Handbook, Front End System Design (the curriculum row's URL): https://www.frontendinterviewhandbook.com/front-end-system-design (
fih-overview.txt). "Interface definition (API): Define the interface (API) between components in the product, functionality of each API, their parameters and responses" (step 3). - Front End Interview Handbook, User Interface Components (the same guide's page for component rounds): https://www.frontendinterviewhandbook.com/front-end-system-design/ui-components (
fih-ui-components.txt). "Do not start drawing the architecture before you are clear about the requirements!" (step 1); "If there are search inputs involved, each keystroke should not fire a network request." (step 4); "Does your component support RTL (right to left) languages like Arabic and Hebrew?". - W3C WAI-ARIA Authoring Practices, Combobox Pattern (the curriculum row's URL): https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ (
apg-combobox.txt). "DOM focus remains on the combobox" (the build, linked touibuild-typeahead). - web.dev, Interaction to Next Paint (INP) (the curriculum row's URL): https://web.dev/articles/inp (
webdev-inp.txt). "An INP below or at 200 milliseconds means a page has good responsiveness." (why each keystroke's own work stays small; linked toperf-inp). - MDN, String.prototype.normalize(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize (
mdn-normalize.txt). "NFD": "Canonical Decomposition."; "NFC": "Canonical Decomposition, followed by Canonical Composition.". - MDN, Intl.Collator() constructor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator (
mdn-collator.txt). "Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A." (step 7: the kind of matching a search service does when it ignores case and accents).
Related lessons
Checked against the tools and docs as of 2026-10-04.