Search autocomplete — the "design top k" problem — is the little list of suggestions that pops up while you type in a search box. It looks simple, but interviewers love it because the hard parts are hidden: you have to rank suggestions by how popular they are, answer in under 100 ms (so the box does not feel laggy), and handle about 48,000 searches per second without crushing your database. The key trick is to stop using a normal database for reads and use a trie instead (more on what that is below).
The animation on the right shows both halves of the system. The online path is what happens
live as you type: a user types t then r, we walk down a trie to the matching spot, read the
ready-made top-5 list stored there, and send the suggestions back — and a popular prefix comes
straight from the cache. The offline path is the background job: search logs are counted up,
and workers rebuild the trie once a week.
Step 1: Ask questions and estimate the scale
Before designing anything, nail down exactly what you are building. Here are the standard answers for this problem:
- Match only the start of what the user typed (a "prefix match", not a search-anywhere match).
- Return the top 5 suggestions, ranked by how often each one has been searched before (its popularity).
- No spell check or autocorrect.
- Only lowercase English letters — no capitals, no symbols.
- 10 million daily active users (people who use it each day).
The non-functional requirements are really the whole point here. The system must be: fast (Facebook aims to show results within 100 ms, or the box feels laggy), relevant, sorted by popularity, able to grow (scalable), and rarely down (highly available).
Quick back-of-the-envelope math
The big insight: autocomplete sends a request for every key you press, not once per search. Multiply the number of searches by about 20 characters per query and a quiet workload suddenly becomes 48,000 requests per second. Pointing out that multiplier without being asked shows you know where the real load comes from.
Users = 10,000,000 per day
Searches per user/day = 10
Characters per query ≈ 20 (about 4 words × 5 letters; 1 letter = 1 byte → 20 bytes)
Requests per query ≈ 20 (one request for EACH character typed)
So typing dinner actually sends six requests: d, di, din, dinn, dinne, dinner.
Requests/sec = 10,000,000 × 10 × 20 / 24 / 3600 ≈ 24,000 per second
Peak requests/sec = above × 2 ≈ 48,000 per second
New data per day = 10M × 10 × 20 bytes × 20% new = 0.4 GB per day
Two numbers shape every later choice. First, 48,000 requests per second at peak — a normal database simply cannot sort the top results for every keystroke that fast, so we need a trie plus a cache. Second, only 0.4 GB of truly new searches per day — the data barely changes, so we can build the trie in the background (offline) instead of live.
Step 2: The high-level design
Split the system into two parts, right along the line the math drew:
| Service | Its job | How often it runs |
|---|---|---|
| Data gathering service | Count up raw searches into a popularity list / trie | In the background, in batches (e.g. weekly) |
| Query service | Given what the user typed so far, return the 5 most-searched words | Live, on every keystroke |
The simplest version of the query service keeps a (query, frequency) table in a normal SQL database and answers each keystroke like this:
SELECT query, frequency
FROM frequency_table
WHERE query LIKE 'tw%'
ORDER BY frequency DESC
LIMIT 5;This is fine for a tiny dataset and a nice way to state the problem. But at 48,000 requests per second it breaks: every keystroke makes the database scan for matches and sort them. The deep dive fixes this.
Start with the SQL version on purpose, then show why it fails. Showing the interviewer the obvious answer and why it falls apart at scale is a stronger signal than jumping straight to a trie — it proves you can reason about a bottleneck instead of just reciting the "right" answer.
Step 3: Deep dive — the trie
Trie (prefix tree)
A tree where the top is an empty string, and each step down adds one letter. So the path from the top to any spot spells out a prefix. The name comes from retrieval. Each spot can branch into up to 26 children (one per English letter); unused branches just are not drawn.
A plain trie stores words like tree, try, true, toy, wish, win as shared letter-paths. To rank them, we store a search count at the end of each word. Here is the basic lookup, where p = how long the prefix is and c = how many words sit under that prefix:
| Step | What we do | Time it takes |
|---|---|---|
| 1 | Walk down to the prefix spot | O(p) |
| 2 | Look at every word under it | O(c) |
| 3 | Sort them and keep the top 5 | O(c log c) |
That works, but in the worst case it scans every word under the prefix on every single keystroke — way too slow for a 100 ms budget. Two fixes solve it:
1. Cap the prefix length. People rarely type very long queries, so limit a prefix to, say, 50 characters. Now finding the prefix spot is basically instant (it goes from O(p) to O(1)).
2. Store the top 5 at every spot, ahead of time. Instead of searching the words under a prefix, precompute the top 5 for each spot and store them right there. Answering a prefix becomes just reading that little list — no searching, no sorting.
spot "be" → [best:35, bet:29, bee:20, be:15, beer:10]
With both fixes, the whole lookup becomes O(1) (constant time, no matter the data size):
| Step | After the fix |
|---|---|
| Find the prefix spot | O(1) |
| Return the top 5 | O(1) (already stored at the spot) |
This trades more memory for more speed — every spot now carries a top-5 list — but for a feature whose entire job is to answer in under 100 ms, that trade is worth it.
"Store the top 5 at every spot" is the single most important line in this design. Be ready to name its downside out loud: it uses much more storage, and when one word's count changes, you must update every spot above it (because they all store a top list that might include it). Naming this "write amplification" cost is what separates a memorized answer from a real understanding.
Step 4: Deep dive — building the trie in the background
Updating the trie on every one of billions of daily searches would crush the live service, and the top suggestions barely move from day to day. So the data gathering service runs in the background, in batches:
Search Logs → Aggregators → Counted Data → Workers → Trie DB → Trie Cache
- Search Logs — raw records of every search, only ever added to (never edited), and not indexed.
- Aggregators — roll that flood of logs up into a popularity count. How often they run depends on how fresh the data must be: fast-moving products (like Twitter) count in short windows; for most cases once a week is enough. We assume the trie is rebuilt weekly.
- Workers — background servers that build the trie from the counted data and save it to the Trie DB.
- Trie DB — where the trie is stored on disk. Two options:
- Document store (e.g. MongoDB): save the whole weekly trie as one big blob.
- Key-value store: use each prefix as a key and that spot's data as the value.
- Trie Cache — an in-memory copy of the trie for fast reads; it takes a weekly snapshot from the DB.
Step 5: Deep dive — the query service and extras
Here is the improved live read path:
- The request hits the load balancer (the traffic director).
- It sends the request to an API server.
- The API server reads the trie data from the Trie Cache and builds the suggestion list.
- If the cache is missing the data (it crashed or ran out of memory), the server reads from the Trie DB and refills the cache, so every later request for that prefix is a fast hit.
A few extra ways to make the read path faster:
- AJAX requests — fetch the suggestions without reloading the whole page.
- Browser caching — suggestions change slowly, so let the browser remember them. Google tells the browser to keep them for one hour, for that one user only.
- Data sampling — at huge scale, logging every keystroke is wasteful, so log only 1 out of every N requests.
Trie operations
| Operation | How it works |
|---|---|
| Create | Workers build it from the counted search data. |
| Update | Option 1: rebuild weekly and swap the new trie in (preferred). Option 2: edit one spot in place — slow, because every spot above it must be updated too. |
| Delete | Put a filter in front of the Trie Cache to strip hateful, violent, or explicit suggestions as they are read; remove them from the DB later, before the next rebuild. |
Scaling the storage
When the trie grows too big for one server, split it across several (this is "sharding"). The naive split is by first letter (a–m on server 1, n–z on server 2; split finer for more servers, up to 26). The catch: far more words start with c than with x, so this gives an uneven split. The fix is a shard-map manager that looks at real search volume and groups letters by load — for example, one server just for s, and one server for u–z together if their totals are similar.
When you reach sharding, raise the uneven-split problem before the interviewer asks. Saying "shard
by first letter" and then immediately "but c is way bigger than x, so I would use a shard-map
manager driven by real search volume" is the senior move — spotting the imbalance yourself is the
signal.
Wrap-up
Search autocomplete is all about moving work off the hot path (the part that runs on every keystroke). A SQL LIKE … ORDER BY … LIMIT can describe the problem but cannot serve 48,000 requests per second in 100 ms, so we precompute. A trie with the top 5 stored at every spot makes reads O(1); building it in the background, weekly keeps the heavy work off the live path; and a cache, browser caching, and sampling trim the rest.
Common follow-up questions and the one-liners that answer them:
| Follow-up | Answer |
|---|---|
| Multiple languages? | Store Unicode characters in the trie spots. |
| Different top searches per country? | Build one trie per country, and serve them from CDNs to keep latency low. |
| Real-time / trending searches? | A weekly rebuild is too slow; shard to shrink the working set, count recent searches more heavily, and switch to stream processing (Kafka / Spark / Storm). |
A clean 45-minute plan: 5 min on requirements plus the per-keystroke math → 5 min on the naive SQL design and why it breaks → 15 min on the trie deep dive (counts on each spot, then the two O(1) fixes) → 10 min on the background build plus the query service → 10 min on scaling, deletes, and the multi-language / trending follow-ups. The trie fixes are where you spend your best minutes.
Practice
Try these to check you understood how autocomplete is served.
1. Why does autocomplete create about 48,000 requests per second?
2. What is a trie (prefix tree)?
3. Which trick makes the trie lookup O(1) (constant time)?
4. Why is the trie rebuilt in the background (offline) instead of live?
5. What problem comes from sharding the trie simply by first letter?