Design complete systems / CHAPTER 55
Design typeahead suggestions
Suggest matching popular queries while someone types, using prepared prefix lists instead of rescanning every saved term.
Builds on Database indexes, Caching fundamentals, Batch versus stream processing.
- Build a prefix index that reads a short candidate list for each query.
- Keep suggestion responses fast while popularity changes in the background.
- Handle popular prefixes, blocked terms and responses that arrive out of order.
01Functional requirements
A typeahead service suggests complete queries while someone types. The prefix is the text already entered: for ca, possible completions include car and camera.
FR1: Prefix suggestions. Return up to ten completions matching the submitted prefix and language. Normalize stored terms and queries in the same way, such as applying the same case and Unicode rules, so equivalent text matches consistently. A popular term that does not match the prefix is not a valid completion.
FR2: Popularity ranking. Order matching terms by recent completed-search popularity within the requested language. Use a declared popularity window rather than assuming lifetime counts represent current interest.
FR3: Phrase suppression. Remove disallowed phrases from every returned list, including lists obtained from a cache or an older popularity index.
FR4: Completed-search collection. Accept completed-search events so future rankings can improve. Count a repeated event ID only once, and apply authentication or abuse controls. Recording a private query does not automatically authorize publishing it as a suggestion.
Out of scope
- The search-results page and document retrieval.
- Personal recommendations and spelling correction. Explain their additional inputs and integration if asked; prefix matching alone supplies neither.
Example. Completed-search counts are car:50, cat:30, camera:20, cart:10, and dog:40. For ca, the first three suggestions are car, cat, camera. dog fails the prefix requirement even though it is popular.
02Nonfunctional requirements
These are independently chosen exercise assumptions to validate, not measured production guarantees.
NFR1: Lookup latency. At least 95% of measured requests must finish within 100 ms, from service receipt to completed response; this is the p95 latency target. The client can also wait briefly for a typing pause before sending a request, a technique called debouncing. Measure that extra wait separately because the user experiences it too.
NFR2: Peak lookup capacity. Plan for about 2,310 prefix requests/second at the assumed peak derived below. Load-test by locale and prefix length, since hot one-character prefixes can be slower than the overall average.
NFR3: Popularity freshness. Ordinary completed searches must affect the suggested ranking within one minute. Measure from saving the completed-search event to serving a validated index build containing its updated count.
NFR4: Suppression correctness. Once current policy blocks a phrase, no returned list may include it. Check that policy after retrieving candidates, including cached candidates. If the check is unavailable, omit unchecked suggestions rather than reuse an older allowed list.
NFR5: Version-swap memory. For a measured 500 MB index, budget at least 1 GB per replica for overlapping old and new versions, plus process and cache overhead. Measure peak memory during validation and installation, not only steady-state index size.
03Back-of-the-envelope Estimates
Estimate the two kinds of work separately: the browser asks for suggestions while typing, but popularity changes only when someone completes a search. All sizes below use decimal bytes.
- Suggestion requests. Assume one million daily users, four completed searches each and five suggestion calls per search.
1,000,000 × 4 × 5 = 20 million calls/day. Divide by 86,400 to get about 231 calls/second; assume a tenfold peak of 2,310/second. Test the suggestion handler at that rate before deciding how many copies to run. - Response bytes. Ten suggestions at 40 bytes each make about 400 bytes per list.
20 million × 400 = 8 GB/day, before JSON and protocol overhead. The service needs fast small lookups rather than a large-file delivery system. - Term text. At 500,000 terms averaging 30 bytes, text alone uses
500,000 × 30 = 15 MB. If a term contributes to at most 20 prefixes, there are at most ten million prefix contributions. Shared prefixes reduce the number of distinct trie nodes. Candidate IDs, scores and pointers can cost more than the text; measure the whole structure, including the proposed 20 candidates at 16 bytes each per distinct prefix. - Installing an index. Suppose the measured structure is 500 MB. A server holding the old and new copy during replacement needs at least
500 + 500 = 1,000 MB, or 1 GB, before process, cache and loading buffers. Measure that peak, not only steady-state memory. - Popularity updates. Four million completed searches/day are about
4,000,000 ÷ 86,400 ≈ 46 events/second. Count each completed search once. Counting the five typing requests as five searches would change the meaning of popularity.
04API Design and Retry Contracts
The browser’s suggestion code sends the text currently in its input box. The server’s suggestion handler returns possible completions; it does not run the final document search.
GET /suggest?q=ca&locale=en&limit=3
{"prefix":"ca","indexVersion":42,"suggestions":[{"term":"car","score":50},{"term":"cat","score":30},{"term":"camera","score":20}]}
- Read the inputs. The handler reads
q,localeandlimit. Herecais the typed prefix,enchooses English and 3 is the requested result count. Apply the same case and Unicode rules used to store terms. - Return a recognizable answer. Return
200with matching terms, including an empty list when nothing matches. Echocaso the browser can recognize a reply to an older input.indexVersion:42identifies the installed index build; it is not a permission decision. - Reject invalid or excessive work. Return
400for an invalid limit and429for throttled callers. Cap prefix length and result count so one call cannot demand an unlimited search. - Collect completed searches separately. After a user actually searches, the completed-search endpoint receives an event ID, normalized term, language and time. The client keeps the same event ID if it retries after losing the reply. The collector authenticates or applies abuse controls. A private query is not automatically safe to publish as a suggestion.
05Data Model and Access Paths
Keep the text, its popularity and the block decision separate. A term ID identifies one stored phrase; a build number identifies one prepared ranking. The suggestion handler uses those IDs to look up data, rather than treating every saved copy as current policy.
| Record / key | Stored values | Access path |
|---|---|---|
Term (locale,termId) |
Normalised text, display text | Resolve candidate IDs |
Count (locale,termId,window) |
Deduplicated count, window end | Build recent popularity |
Prefix (locale,prefix,indexVersion) |
Ranked candidate IDs and scores | Direct lookup on each keystroke |
Suppression (termId) |
Current blocked state and version | Final serving check |
- The count worker counts completed searches within a stated interval, such as the last hour, with a longer-term fallback. Otherwise old lifetime popularity could dominate forever.
- The count store also retains processed event IDs for as long as those events can be retried or replayed. The worker saves “event counted” and the count change together, so a retry cannot add the same completed search twice.
- The index builder reads those counts and prepares the prefix records. A prefix record stores several possible answers, called candidates, not just the number the browser displays.
- The policy store records which term IDs are currently blocked. The suggestion handler checks it after loading candidates from either memory or a cache.
06Baseline: Start with a prefix index
First make one server answer ca correctly. A small vocabulary can begin as a database table: select terms starting with ca, sort them by popularity and take the first results. A limit of three bounds the answer size, but the database may still inspect and sort many matches.
A trie stores characters along branching paths. Following the c branch and then a reaches the node for ca. Prepare a short ranked list there so the handler does not search every descendant on each keystroke.
Prepare the server
- The builder reads the saved terms and counts:
car50,cat30,camera20,cart10 anddog40. - It places
car,cat,cameraandcartunderca, ordered by score.Dogis absent because it does not start withca. - Before publishing the prepared index, the builder checks known prefix queries, score order, expected vocabulary coverage and memory size. The server loads that completed build into memory. Requests only read it; they do not change its branches or scores. This is an immutable index.
Handle one browser lookup
- The suggestion handler reads
cafrom the browser’s request and followsc, thenain memory. - It loads
[car:50, cat:30, camera:20, cart:10]from that node. - It asks the current block policy which of those term IDs may be returned. If
caris blocked, it removescar. - It returns
cat,cameraandcart, plus the prefixca. Keepingcartas a reserve lets filtering still produce three answers. - The browser compares the response prefix with its current input. It displays the list only if the input is still
ca.
If the node stored only three candidates, removing car would leave only cat and camera. The handler must return two or consult another candidate source; it cannot guess that cart is fourth. A compressed trie can combine nonbranching character paths to save memory, but needs more involved construction.
Trace ca from stored candidates to a permitted answer
Follow the numbered arrows
- Prefix ca → Trie node catraverse characters
- Trie node ca → car 50; cat 30read ready-made list
- car 50; cat 30 → Current block checkcheck current policy
- Current block check → cat; camera; cartremove car; take three
Trace the diagram in words
- The suggestion handler follows `ca` to its stored ranked list of matching terms.
- Filtering removes car without waiting for a popularity rebuild.
- The reserve cart can fill the third slot only because it was retained.
BUILD THE DESIGN, ONE CHANGE AT A TIME
Watch the architecture evolve.
Advance through the stages in this same diagram. Existing components keep their places; additions fill the available space, then add rows. Highlights show what changed from the previous stage, and numbered arrows match the connection key below.
Stage 1 of 3
Start here. First make one server answer ca correctly using a vocabulary that fits its measured memory.
Small prefix service
Follow the numbered arrows
- Typing client → Suggestion serviceq=ca, locale=en, limit=3
- Suggestion service → Serving prefix indexread ranked reserve IDs
- Suggestion service → Suppression authoritycheck candidates before return
- Suggestion service → Typing clienteligible terms + echoed prefix
- Persisted search counts → Serving prefix indexstartup/in-process snapshot build
Trace the diagram in words
- The builder reads saved counts and loads one validated index into the server.
- Browser code sends ca; the suggestion handler reads its reserve list.
- The handler removes blocked terms and echoes ca so the browser can reject an old reply.
- The builder saves the term counts and prepares a read-only prefix index.
- The suggestion handler loads a candidate list and checks current blocks before replying.
The cost: One server keeps the request path simple, but it has a capacity limit and its failure interrupts serving.
Why change it? One server is reaching its tested lookup capacity, while the complete index still fits in each server’s memory.
Replicated validated index
Follow the numbered arrows
- Typing client → Suggestion serviceq=ca, locale=en, limit=3
- Suggestion service → Serving prefix indexread ranked reserve IDs
- Suggestion service → Suppression authoritycheck candidates before return
- Suggestion service → Typing clienteligible terms + echoed prefix
- Persisted search counts → Index builderbuild from retained snapshot
- Index builder → Version storagepublish validated immutable version
- Version storage → Serving prefix indexload then switch serving pointer
Trace the diagram in words
- The builder includes camera’s new score in a replacement build.
- Version storage retains the validated output; each server installs it when ready.
- The handler still checks the current block policy after reading any build.
- A separate builder prepares and validates replacement builds without slowing lookups.
- Several serving processes each load the complete index; a load balancer chooses one per browser request.
- Each server keeps old and new builds during installation and retains a valid rollback version.
Inspect the connection changes
- Now: Persisted search counts → Index builder: build from retained snapshot
- Now: Index builder → Version storage: publish validated immutable version
- Now: Version storage → Serving prefix index: load then switch serving pointer
- Previously: Persisted search counts → Serving prefix index: startup/in-process snapshot build
All other connections are retained. Response paths are implicit unless drawn.
The cost: Each replica repeats the index’s memory cost. The team must monitor build delay, installation success and peak memory.
Why change it? Repeated requests for the same prefix repeat the same index reads. If waiting for count updates also delays completed-search collection, cache candidate lists and save search events for a separate counting program.
Cached event-fed serving
Follow the numbered arrows
- Typing client → Suggestion serviceq=ca, locale=en, limit=3
- Suggestion service → Suppression authoritycheck candidates before return
- Suggestion service → Typing clienteligible terms + echoed prefix
- Persisted search counts → Index builderbuild from retained snapshot
- Index builder → Version storagepublish validated immutable version
- Version storage → Serving prefix indexload then switch serving pointer
- Suggestion service → Candidate cachelookup versioned candidates
- Completed-search log → Persisted search countsdeduplicate and aggregate completed searches
- Suggestion service → Serving prefix indexcache miss: read bounded index
- Candidate cache → Suggestion servicehit:versioned candidate IDs
Trace the diagram in words
- The collector records one completed search, keeping its ID stable on retry.
- The count worker skips an ID already counted and updates new IDs exactly once in its count store.
- The handler obtains cached or indexed candidates, checks current blocks and returns matching permitted terms.
- The collector saves completed-search records in a durable log for a separate counting program.
- The counting program records each processed event ID with its count change to prevent a retry from counting twice.
- A candidate cache saves repeated prefix reads; the handler still checks current blocks after a cache hit.
Inspect the connection changes
- Now: Suggestion service → Candidate cache: lookup versioned candidates
- Now: Completed-search log → Persisted search counts: deduplicate and aggregate completed searches
- Now: Suggestion service → Serving prefix index: cache miss: read bounded index
- Now: Candidate cache → Suggestion service: hit:versioned candidate IDs
- Previously: Suggestion service → Serving prefix index: read ranked reserve IDs
All other connections are retained. Response paths are implicit unless drawn.
The cost: The log, count worker and cache need recovery and monitoring. Caching reduces repeated lookups but does not make popularity updates immediate.
07Evolution: Split updates from serving
Change the part that reaches its measured limit. A larger machine may hold more index data and serve more lookups, but still needs an independent recovery copy.
When one server cannot serve enough lookups
- Run several suggestion servers behind a load balancer. Give each the complete small index, so one browser lookup reaches one server rather than contacting many data partitions.
- Run the builder separately from those handlers. To add
camera’s 50 new searches, it reads updated counts, computes20 + 50 = 70and prepares a replacement build. - Validate the replacement and save it in version storage. Each serving process switches from build 41 to 42 only after the new copy is ready; reads already using 41 finish there. Keep the previous valid build for rollback and budget both copies in memory.
When update collection or repeated prefixes add unnecessary work
- The completed-search collector saves events in a durable log: a retained sequence of records the count worker can read again after a crash. This lets collection and counting run at different speeds without losing accepted updates.
- For each event, the count worker checks its stored processed-event ID. A matching retry is skipped; a new ID updates the window’s count and is marked processed in the same transaction. This is deduplication: recognize repeated delivery before repeating its effect.
- The builder uses those counts for the next ranking.
Cameracan move ahead ofcarandcatwhen that build is installed; current blocking still happens on every lookup, independently of the build. - Cache frequent candidate lists under
(locale,prefix,version). A cache hit saves index work, but the handler must still run the current block check.
| Choice | Work saved | Added cost or limit |
|---|---|---|
| Database prefix query | Avoid a separate prepared index initially | Broad prefixes can inspect and rank many rows |
| Prepared trie lists | Read a short ready-made list | Extra memory and delayed popularity publication |
| Full serving replicas | Spread browser requests without a multi-part query | Repeat the complete index memory on each server |
| Partition by language or prefix range | Fit a larger vocabulary across machines | Popular short prefixes can overload one partition |
Do not assume c and z receive equal traffic. Replicate very popular short prefixes when useful. Personalization would need additional user data and a bounded scoring step; the shared popularity index alone does not supply it.
08Failure and Recovery Scenarios
Test the basic lookup before adding these failures:
- The browser receives replies in the wrong order. It sent
ca, thencat, but thecareply arrives last. Browser code checks the echoed prefix and ignores that reply; arrival time does not make it the newest input. - The builder fails. Serving processes continue reading the last validated build. Operators measure its age and alert when the minute freshness target is missed. For example, a build that unexpectedly loses the saved vocabulary must fail the coverage check even if its file format is readable.
- An older build arrives late. A serving process already using 43 rejects installation of 42. Compare build numbers before changing the active index, so delayed work cannot reverse a newer publication.
- The block check is unavailable. The handler cannot prove which candidates are permitted. It omits unchecked suggestions instead of returning a previously allowed list. This protects the removal rule at the cost of some availability.
- A deletion must remove stored history. Blocking a response does not erase saved events or old index builds. The retention process must remove the required copies as well and verify that it did so.
09Trade-offs and Validation
The design spends memory and build work before a request, so the suggestion handler reads a short list after each typing pause. That fits a shared popularity ranking. Arbitrary filters or a unique ranking over millions of terms would require a different candidate plan.
- Check response speed: measure service p95/p99 and the extra wait the browser adds before sending. Break results down by language and prefix length; one-character prefixes can be much busier.
- Check update progress: record the installed build number, build age, completed-search delay and memory during replacement.
- Check correctness: count blocked terms returned, prefix mismatches and answers shortened by filtering. A high cache hit rate cannot excuse a forbidden term.
- Check usefulness: measure accepted suggestions with controlled ranking experiments. Higher positions receive more clicks even without better relevance.
Close by explaining the ca lookup, the blocked car and the late browser reply. Spelling correction needs a way to generate nearby words; document retrieval needs a separate result index. A trie supplies neither automatically.
OPTIONAL DESIGN EXTENSIONWhere AI Fits in This Design
A model may optionally reorder candidates that the prefix handler already found. It must not invent a new phrase or grant permission to return a blocked one.
- Input and output. Send a bounded list of permitted term IDs, popularity scores, language and approved aggregate features. The model returns an order of those same IDs. For
ca, it might placecamerabeforecat. - Where it runs. The suggestion handler calls the model after matching and checking the block policy. It validates the returned IDs and checks current suppression again before replying;
carstays excluded if policy blocks it. - Fallback and budget. Assume at most 10 ms of the 100 ms service budget for inference. If the model times out or returns unknown IDs, use the validated popularity order. Measure the budget instead of assuming every model fits.
- Evaluation and privacy. Compare relevance and suggestion acceptance across languages, controlling for position bias. Track slow responses and cost per thousand calls. Use minimized aggregate signals; private queries or individual histories require explicit collection and retention permission before training or publication.
The ordinary trie lookup remains useful when the model is unavailable.
DRAW. CALCULATE. EXPLAIN.
Build and challenge a tiny suggestion index
75 minutes · guided practice
Use the terms cat (30), car (50), camera (20), dog (40), and cart (10). Draw a trie and store the best two terms at each relevant prefix. Then block car, add 50 new searches for camera, and show which answers change immediately versus after the next validated build. Finally describe how the client handles responses arriving out of order. State the answer first with exactly two stored candidates, then show what changes if camera is retained as a third reserve.
Compare the worked solution
- Build the two-candidate lists. At
candca, storecar(50) andcat(30). Atcar, storecar(50) andcart(10). Each list contains the highest scores among terms matching that prefix. - Apply the block at lookup time. The suggestion handler removes
carfrom the stored list. It can return onlycatforc/ca, and onlycartforcar; it has not retained another candidate. - Add a reserve deliberately. Retain
camera(20) as a third candidate atc/ca. Now the same filter leavescatandcamera, filling a two-result answer without guessing. - Publish the popularity change. The count worker records 50 new completed searches, making
camera’s score20 + 50 = 70. The builder prepares and validates a new index. After installation,cameraandcatcan be the two highest permitted results. The popularity update waits for installation; the block check does not. - Handle a late reply. Once the user types
cat, browser code ignores a response echoingca. During index replacement, reserve memory for both builds until older reads finish. - Explain the choice. The trie spends memory and preparation work to avoid searching and sorting all matching terms on every request. Compare that cost with the small database prefix baseline.
The checkboxes record your own assessment. The worked solution gives you a way to check the reasoning.
RETRIEVE BEFORE YOU REVEAL
Keep the important ideas.
Say the answer aloud, then turn the card over.
CARD 01How does a trie make a suggestion lookup predictable?
The suggestion handler follows the typed characters to a trie node and reads its prepared ranked terms. It removes currently blocked terms before returning the list. Keeping extra candidates lets another permitted term fill a removed term’s place.
CARD 02Which two kinds of freshness must stay separate?
The builder publishes new popularity scores in a validated index build. The handler separately checks current blocks on every lookup, including cache hits. A recent build number does not prove that a term is still permitted.
CARD 03When should the small index be replicated rather than partitioned?
Give each server the full index while it fits in memory and measured request traffic is the limit. Divide the vocabulary when one server cannot hold it. Measure popular-prefix traffic and keep enough memory for old and new builds during replacement.
CHECK THE REASONING
Can you explain the choice?
Answer every question, then finish for an immediate score and an explanation for each answer. Aim for 80%, then revisit any gaps.
CLOSE THE LOOP
Explain it without the page.
Summarise the mechanism, one trade-off, and one failure mode. Then mark this chapter complete.