System design interview: design search autocomplete
Search autocomplete returns the few most likely completions of whatever a user has typed so far, fast enough to keep up with typing: about 100 ms from request to rendered list. Design it as two systems: an offline pipeline that aggregates query logs into ranked suggestions, and an in-memory trie that stores the top k completions at every node, so serving a prefix is one short walk with no searching. Cache short prefixes aggressively, because a small set of them receives most of the traffic.
By Evin JohnPublished 7 min read
1. Clarify the requirements
Autocomplete sounds small, so interviewers use the scoping questions to see whether you notice the hard parts: latency, freshness and what must never be suggested.
- Functional: as the user types, return up to 5 to 10 completions ranked mainly by popularity; filter offensive and unsafe suggestions; optionally blend in the user's own recent searches; support several languages.
- Non-functional: about 100 ms from sending a request to showing the list, measured at the 99th percentile; very high availability, but failure can be quiet (no dropdown is better than a slow one); suggestions may lag real search behavior by minutes or hours.
- Out of scope unless asked: spelling correction and fuzzy matching, the search results page, and ads in the suggestion list.
2. Estimate the scale
Requests come from keystrokes, not searches, which multiplies traffic. The numbers below are assumed for illustration.
| Quantity | Assumption | Result |
|---|---|---|
| Searches | 100 million daily users x 10 searches | 1 billion searches a day |
| Suggestion requests | 6 requests per search after debouncing | 6 billion a day, about 70,000 a second; about 140,000 at a 2x peak |
| Requests reaching servers | half answered by browser and CDN caches | about 70,000 a second at peak |
| Distinct queries indexed | top 100 million, 20 characters on average | about 2 GB of query text |
| Trie size | 500 million nodes after shared prefixes, about 100 bytes each (children plus 10 suggestion IDs) | about 50 GB in memory |
| Query logs | 50 bytes per search, 1 in 10 sampled | 50 GB a day raw, 5 GB a day sampled |
A 50 GB index fits on one large machine, but not comfortably with room to build the next version beside it, and a single machine cannot serve 70,000 requests a second with headroom. So the index is split into a few shards, each replicated several times. The 100 ms budget is mostly network: a mobile round trip can take 50 ms or more, which leaves the server roughly 10 to 20 ms.
3. Define the API
GET /v1/suggest?q=how%20to%20m&lang=en&limit=8
returns: 200
Cache-Control: public, max-age=300
{ "prefix": "how to m",
"suggestions": [ { "text": "how to make pancakes" },
{ "text": "how to meditate" } ] }
GET /v1/suggest/recent?q=how%20to%20m (signed in, Cache-Control: private)
returns: 200 { "suggestions": [ { "text": "how to migrate postgres" } ] }Normalize the prefix before using it as a cache key: lowercase, collapse whitespace, apply Unicode normalization. Otherwise "How to m" and "how to m" are cached separately.
4. Sketch the data model
| Item | Shape | Why it is there |
|---|---|---|
| Trie node | children (character to node), topK (list of query IDs) | The serving structure; the top k list makes a lookup constant work after the walk |
| Query dictionary | query_id to text, language, flags | Nodes store 4-byte IDs, not repeated strings |
| Aggregated counts | query, day, count, decayed score | Input to each build; old days fade out with a decay factor |
| Query log | time, normalized query, language, region, whether a suggestion was picked | Raw input to the pipeline; short retention for privacy |
| Blocklist | term or pattern, reason | Applied when building and again when serving |
| Recent searches | user_id to last 50 queries | Optional personalization, in a separate store |
A popular alternative to a custom trie is a flat table from prefix to its top k list, stored in a key-value store and capped at, say, 20 characters. It is the same idea with the tree flattened out: more duplication, but trivial to shard, cache and operate.
5. Make each lookup one short walk
The central decision is how much work happens per keystroke. Compare the options against the latency budget.
| Approach | How it works | Strength | Cost |
|---|---|---|---|
| Trie, search the subtree | Walk to the prefix node, visit every completion below it, sort by count | Small; counts can be live | The prefix "s" has millions of completions below it, so latency is unbounded |
| Trie with top k at each node | Precompute each node's best k completions during the build | Lookup cost is the prefix length; nothing to sort at request time | k IDs per node of extra memory; a count change touches every ancestor, so rebuild offline |
| Flat prefix table | Key-value rows: prefix to top k list | Works with off-the-shelf stores; easy to shard and cache | One row per prefix, heavy duplication; needs a length cap |
| Search engine suggester | A completion feature in a search engine (for example, the one in Elasticsearch) | Off the shelf; fuzzy matching available | Less control over ranking and memory; another cluster to run |
Build top k bottom-up: a node's list is the best k of its children's lists plus the query that ends at the node itself. Each merge is small, so a full build is linear in the number of nodes times k. Rank by a decayed score, for example each day's count multiplied by 0.9 for every day of age, so last month's spike fades without disappearing overnight.
6. The architecture
- Client
- Debounced input
- Local response cache
- Edge
- CDN caching public prefixes
- Load balancer
- Serving
- Suggestion service (routing, merge, filter)
- Trie shards in memory, replicated
- Recent-search store
- Offline pipeline
- Query log stream
- Aggregation job (sampled counts, decay)
- Trie builder
- Snapshot storage
- Control
- Blocklist service
- Snapshot validation and swap
- TypeThe client waits for a short pause in typing, cancels any request still in flight, and ignores a response whose prefix no longer matches the input.
- ServeEdge cache first; on a miss, the service walks the owning shard's trie to the prefix node and returns its top k after filtering.
- LogEach completed search, and whether a suggestion was chosen, goes onto the log stream.
- BuildEvery day (or hour), aggregate counts, apply the blocklist, build a fresh trie and write it as a snapshot.
- SwapShards load the new snapshot beside the old one and flip a pointer, keeping the previous one for rollback.
7. Scale it
- Shard by prefix range, weighted by traffic. Splitting by first letter is uneven: far more queries start with "s" than "x". Choose ranges from query volume ("a" to "ap", "aq" to "b", and so on) and keep the small range map in the router. See sharding for the general pattern.
- Serve the shortest prefixes separately. One- and two-character prefixes are the hottest and there are only a few thousand of them per language. Precompute their lists, hold them on every server and at the CDN.
- Cache at every layer. The CDN key is the normalized prefix plus language and region, with a lifetime of a few minutes. The browser caches responses too. A caching layer only works here because public suggestions are the same for everyone.
- Debounce, but carefully. A 100 ms debounce on top of a 100 ms request makes the box feel slow. Use a short pause (tens of milliseconds) or fire on every keystroke and cancel the previous request. While waiting, the client can filter the list it already has for the shorter prefix as a placeholder, knowing it may be incomplete.
- Sample the logs. Counting 1 in 10 searches keeps popular queries' ranking accurate, drops rare queries that would be pruned anyway, and reduces the personal data the pipeline handles.
- Freshness in layers. A daily full rebuild plus a streaming job that counts the last 15 minutes of queries and serves a small trending list, merged at request time. Only build the streaming layer if the freshness answer in section 1 required it.
8. What breaks, and what you do about it
| Failure | Effect | Mitigation |
|---|---|---|
| A shard's replicas are all down | Prefixes in its range get no suggestions | Several replicas per shard across zones; the client hides the dropdown on timeout rather than waiting |
| A bad snapshot (empty, truncated, or full of junk) | Wrong suggestions everywhere at once | Validate before swapping: size, sample lookups, blocklist hit rate; keep the last good snapshot for instant rollback |
| An offensive suggestion appears | Reputational and user harm | Serve-time blocklist for immediate removal, then a CDN purge for that prefix; fix the build filter afterwards |
| Coordinated searches push a term up | Manipulated or spam suggestions | Count distinct users per query rather than raw searches; drop bursts from few sources |
| Responses arrive out of order | The list shows suggestions for an older prefix | Client compares the response's prefix with the current input and drops stale ones |
9. Trade-offs to discuss
- Freshness or cost. Hourly rebuilds and a streaming layer cost far more than a nightly batch; many products never need them.
- Memory or latency. Storing top k at each node multiplies memory but removes all per-request searching. In a 100 ms budget, memory is the cheaper resource.
- Personalization or cacheability. A personalized list cannot be cached at the CDN. Keep the public list cacheable and fetch or merge a small personal list separately.
- Popularity or safety. Pure popularity surfaces whatever people type most, including harmful text. Filtering costs some relevance and needs human review of edge cases.
10. How to talk through it in the interview
Fictional example
- Interviewer
- Why store the top ten at every node? That is a lot of duplicated data.
- Candidate
- Because the alternative is searching the subtree on every keystroke, and for a one-letter prefix that subtree holds millions of queries. With the list precomputed, a lookup is a walk of a few characters and a copy of ten IDs, which fits in the 10 to 20 ms the server gets.
- Interviewer
- Then how do you update a count?
- Candidate
- I would not update in place. A daily build recomputes the lists bottom-up and the shards swap snapshots. If we need trending queries within minutes, I would add a small streaming list merged at request time rather than mutate the trie.
- Say early that there are two systems, serving and building, and draw them separately.
- Spend the latency budget out loud: network, server, rendering.
- Mention filtering before the interviewer does.
Practicing this design with Natively
Run this as a mock interview, with someone else playing the interviewer or with you reading the prompt aloud and answering. Natively transcribes the conversation live and can suggest talking points for each question as it comes in; comparing them with what you said is a quick way to spot gaps such as snapshot validation or why personalized lists cannot be cached. A screenshot of your trie sketch can be captured and explained. The transcript and notes remain afterwards, so you can review where the latency budget discussion went off course.
Questions people ask
Why store the top k suggestions at every trie node?
Without them, a short prefix like "s" requires searching a subtree with millions of queries on every keystroke. Precomputing each node's best completions makes a lookup cost only the length of the prefix. The price is extra memory and an offline rebuild whenever counts change.
How often should autocomplete suggestions be updated?
Many products rebuild the index daily or hourly from aggregated query logs and swap it in atomically. If breaking news must appear within minutes, add a small streaming layer that counts recent queries and merges a trending list at request time. Ask the interviewer how fresh suggestions need to be before choosing.
Can personalized autocomplete results be cached at a CDN?
Not safely, because a shared cache would show one user's history to another. Keep the popular, public suggestions cacheable and fetch a small personal list separately, then merge them in the client or service. That keeps the high-traffic path cheap.
Where to go next
Practice with the same assistant you would use on a call: it listens, transcribes and suggests answers from your own resume and the job description.
Facts on this page were last checked on . Product details come from the Natively app itself; if something here is out of date, tell us at hello@heynatively.com.
