Designing Typeahead Autocomplete: Sharded Tries and Precomputed Top-K
Autocomplete has the most hostile latency budget in consumer software. A user typing at 200ms per character fires a query per keystroke, and suggestions that arrive after the next keystroke are garbage — the effective deadline is ~100ms end-to-end, network included. That budget outlaws almost everything: no database query, no scoring pass over candidates, no fan-out to ten services. The design lesson autocomplete teaches better than any other system: when the read deadline is brutal, move all the work to write time.
Requirements
Suggest top 5-10 completions per prefix from a corpus of billions of historical queries; p99 under ~50ms server-side; suggestions reflect popularity and recency (a breaking-news term should surface within minutes-hours); personalize lightly; filter unsafe content.
The read path: a lookup, not a computation
The core structure is a trie over query strings — but the naive version (walk to the prefix node, DFS the subtree, collect completions, rank by frequency) does unbounded work per keystroke: prefix "a" has a subtree with hundreds of millions of descendants. DFS-at-read-time is the answer that fails the interview.
The fix: store the answer in the node. Each trie node holds its precomputed top-K completions.
node "app" -> ["apple", "apple watch", "app store", "application", ...] (K=10)
read path: hash/walk to node -> return stored list -> done. O(prefix length).
Space math holds up: top-10 per node, with completions stored once and referenced by ID, keeps the whole structure memory-resident even at billions of queries — and memory-resident is non-negotiable at this deadline. Serving nodes hold the trie in RAM, replicated for throughput; a request touches exactly one shard (see below), so p99 is a single in-memory lookup plus network.
Two practical refinements interviewers reward: prefix hashing beats pointer-chasing (store prefix → top-K in a flat hash map; the "trie" is conceptual — cache-friendly, trivially serializable, and shardable by key), and client-side caching of results per prefix, since typing "appl" then backspacing to "app" should not re-query.
Sharding: not A-Z
The instinctive shard-by-first-letter is a skew disaster — 'a' and 's' carry orders of magnitude more traffic than 'x'. Shard by hash of the full prefix instead: hash("app") mod N. Every prefix lives on exactly one shard, load spreads uniformly, and a query still touches one shard (the client asks for the prefix it has, not a range). The trade: prefixes of one word scatter across shards, so there is no subtree locality — which costs nothing, because we never walk subtrees at read time. The precompute decision and the sharding decision reinforce each other; noticing that coupling is senior-level signal.
The write path: where the actual system lives
Suggestions come from query logs, so the pipeline is a streaming aggregation feeding periodic index builds:
search logs -> stream aggregation (count per query, sliding windows)
-> candidate scoring: frequency × recency decay × quality/safety filters
-> offline build: for every prefix of every candidate, compute top-K
-> ship immutable index shards to servers (atomic pointer swap)
The top-K-per-prefix build is a classic MapReduce/Spark job: emit (prefix, query, score) for each prefix of each query (a 20-char query emits 20 pairs), group by prefix, keep K. Rebuild cadence — daily or hourly — is fine for the stable head of the distribution but fails breaking news, so real systems run a two-tier index: the big offline-built base, plus a small real-time overlay fed by the stream (last few hours' trending deltas), merged at read time. Two sorted lists of ~10 merged in-process is nanoseconds; freshness without touching the base build. Same lambda shape as ad-click counting — batch for truth, stream for now.
Scoring inside the build is where product lives: raw frequency plus time decay (score = Σ count_i × e^(-λ·age_i)), geography/language partitioning (separate indexes per locale beats per-query filtering), and the safety filter as a build-time exclusion list — filtering at read time means one config bug serves the unfiltered list.
Personalization stays out of the hot structure: fetch the user's recent-queries list (small, cached) and merge-boost client- or edge-side. Blending a per-user model into the shared index is how you turn one lookup into a scoring pass and blow the budget.
| Interview probe | Answer sketch |
|---|---|
| Why not Elasticsearch prefix queries? | Search engines score at read time; the deadline demands O(1) lookup of precomputed answers |
| Update top-K on every search, online? | Write amplification: one query touches 20 prefix entries under contention; stream + periodic merge wins |
| Multi-word / mid-word matching? | Index suffixes/word-boundaries too (bigger build, same read path) — state the space trade |
| Typos? | Fuzzy matching breaks the precompute; practical answer: small edit-distance expansion at the edge for short prefixes only |
The transferable principle, worth closing an interview with: precompute-at-write vs compute-at-read is a dial, and autocomplete sits at the extreme end — every design choice (per-node top-K, hash sharding, immutable index ships, overlay merges) is that one dial turned all the way.
Keep reading
Consistency Models Beyond CAP: Linearizability, Causal, and Session Guarantees
'Strong vs eventual' is a cartoon. The real spectrum — linearizable, sequential, causal, session guarantees — and how to pick per operation, not per system.
Designing an API Gateway: The Front Door as a System
Authentication, routing, rate limits, transformations, and a plugin chain — in a tier that must add ~1ms and never be the outage. Envoy/Kong architecture from scratch.
Designing a CDC Pipeline: Change Data Capture from Binlog to Downstream
Dual writes are a lie; the database's own log is the truth. Log-based CDC, the snapshot-plus-stream handoff, schema evolution, and ordering guarantees that survive resharding.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.