Requirements & scope
Problem Statement & Requirements
Functional Requirements
- Prefix matching -- as the user types each character, return the top suggestions matching the prefix
- Top-K suggestions -- return the K most popular/relevant suggestions (typically K=5-10)
- Real-time updates -- incorporate trending queries into suggestions quickly (minutes, not days)
- Personalization -- blend user's own search history into suggestions
- Multi-language support -- handle queries in multiple languages and scripts
- Spell tolerance -- handle minor typos in prefix (fuzzy matching)
- Phrase completion -- suggest completions for multi-word queries ("how to b" → "how to bake bread")
- Trending queries -- boost currently trending topics (sports events, breaking news)
- Offensive content filtering -- suppress suggestions for hateful, explicit, or harmful content
- Entity-aware suggestions -- recognize and boost entities (people, places, brands)
Non-Functional Requirements
- Ultra-low latency -- suggestions must appear in < 100 ms (ideally < 50 ms)
- High availability -- 99.99% uptime; degraded suggestions are better than none
- Massive scale -- serve 100K+ suggestion requests per second
- Consistency -- eventual consistency is acceptable (different users may see slightly different suggestions)
- Scalability -- handle billions of unique queries in the corpus
- Bandwidth efficiency -- minimize data transfer (users type fast, requests are frequent)
Scope Boundaries
| In Scope | Out of Scope |
|---|---|
| Prefix-based query suggestions | Full search results (covered in Search Engine design) |
| Popularity-based ranking | Complex ML-based ranking (simplified) |
| Real-time trending updates | Ad suggestions in autocomplete |
| Personalization (basic) | Voice-based autocomplete |
| Offensive content filtering | Detailed NLP / LLM suggestions |
| Multi-language support | Cross-language translation suggestions |
Scale estimations
Scale Estimations
Query & Suggestion Corpus
| Metric | Value |
|---|---|
| Unique search queries (all time) | 10B |
| Unique queries with meaningful frequency | 500M |
| Average query length | 20 characters (4 words) |
| New unique queries per day | 50M |
| Trending queries (burst) | 100K at any given time |
Traffic
| Metric | Value |
|---|---|
| Daily Active Users (DAU) | 500M |
| Searches per user per day | 5 |
| Total searches per day | 2.5B |
| Keystrokes per search (avg) | 10 (users type partial, then click suggestion) |
| Autocomplete requests per day | 2.5B × 10 = 25B |
| Requests per second (avg) | ~290K RPS |
| Requests per second (peak) | ~600K RPS |
Storage
| Metric | Value |
|---|---|
| Suggestion corpus (500M queries × 20 bytes avg) | ~10 GB |
| Trie structure overhead | ~30 GB (pointers, metadata) |
| Per-user personalization (100M active × 1 KB) | ~100 GB |
| Frequency/popularity scores | ~4 GB |
| Total in-memory dataset | ~50 GB per replica |
Bandwidth
| Metric | Value |
|---|---|
| Average response size (10 suggestions × 30 chars) | ~500 bytes |
| Outbound bandwidth | 290K × 500 bytes = ~145 MB/s |
| Inbound bandwidth (prefix queries) | 290K × 50 bytes = ~15 MB/s |
Layered architecture
High-Level Architecture
High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Detailed Architecture
Detailed ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
API & contracts
Workshop note · added for the website’s common reading format
For Typeahead & Autocomplete, define contracts around the boundary components: Search box, Edge cache, Suggestion service. Specify authentication, request identity, versioning, pagination or streaming semantics, timeouts, and retry behavior. Name which operations are idempotent and how callers discover an uncertain outcome.
The source discusses these contracts within its subsystem walkthroughs rather than as a standalone endpoint catalog. The examples in this article are design exercises, not published service APIs.
Data model
Workshop note · added for the website’s common reading format
Identify the authoritative records behind Trie snapshots, Query log, Aggregation + build. For each record, define its identity, access pattern, partition key, retention, and version. Separate durable truth from rebuildable indexes and caches.
Trace the most important read and write through the request-flow and core-decision sections before choosing a schema. Avoid adding a distributed transaction unless the invariant truly requires it.
Core design decisions
Trie Data Structure (Core)
Basic Trie Structure
Basic Trie StructureExcalidraw diagram · editable shapes · reveal step by stepExplore
Trie Node Structure
struct TrieNode {
children: HashMap<char, TrieNode>, // or array[128] for ASCII
is_end_of_query: bool, // marks complete query
frequency: u64, // search frequency count
top_k: Vec<(String, u64)>, // precomputed top-K suggestions
// for prefix ending at this node
}
Memory per node:
- children pointers: ~256 bytes (hash map) or 1 KB (array)
- is_end + freq: 9 bytes
- top_k (10 entries): ~400 bytes (query strings + scores)
Total: ~700 bytes - 1.5 KB per node
For 500M unique queries × avg 20 chars:
- Upper bound nodes: 10B (but massive sharing of prefixes)
- Actual nodes (shared prefixes): ~2-3B
- Memory: 2B × 1 KB = ~2 TB ← TOO MUCH for naive trieOptimized Trie: Compressed Trie (Patricia / Radix Tree)
Optimized Trie: Compressed Trie (Patricia / Radix Tree)Excalidraw diagram · editable shapes · reveal step by stepExplore
Further Optimization: Top-K Precomputation
Instead of storing full queries at each node, store only the top-K suggestion IDs:
struct CompactTrieNode {
children: Vec<(char, u32)>, // (char, child_offset) - sparse
top_k_ids: [u32; 10], // indices into suggestion string pool
flags: u8, // is_terminal, has_children, etc.
}
Separate string pool:
suggestions: Vec<String> // all 500M suggestion strings
scores: Vec<u32> // corresponding popularity scores
Memory per node: ~60 bytes
Total nodes: ~500M
Total trie structure: ~30 GB ← fits in memory!
String pool: ~10 GB
Total: ~40-50 GB per replicaTop-K Propagation Algorithm
Building the trie with top-K at each node:
1. Insert all (query, frequency) pairs into the trie
2. Bottom-up traversal:
For each node N:
candidates = []
if N.is_terminal:
candidates.append((N.query, N.frequency))
for child in N.children:
candidates.extend(child.top_k)
N.top_k = heapq.nlargest(K, candidates, key=frequency)
Example:
Node "app" has top_k:
[("apple", 50K), ("apple pie", 30K), ("app store", 25K),
("apple music", 20K), ("application", 15K), ...]
Query time: O(L) where L = prefix length
- Walk the trie character by character: O(L)
- Return precomputed top_k: O(1)
- Total: O(L) ← typically L < 20, essentially O(1)Request flows
Query Flow (Online Serving)
Keystroke-to-Suggestion Flow
Keystroke-to-Suggestion FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Client-Side Optimizations
Client-Side OptimizationsExcalidraw diagram · editable shapes · reveal step by stepExplore
Data Collection & Aggregation Pipeline
Query Log Collection
Query Log CollectionExcalidraw diagram · editable shapes · reveal step by stepExplore
Frequency Scoring Formula
Score(query) = Σ count(query, day_i) × decay(day_i)
i=1..30
Where:
decay(day_i) = e^(-λ × age_in_days)
λ = 0.1 (tunable decay rate)
Example for "apple pie":
Day 0 (today): 5000 searches × e^(0) = 5000
Day 1: 4800 searches × e^(-0.1) = 4342
Day 7: 3000 searches × e^(-0.7) = 1489
Day 30: 2000 searches × e^(-3.0) = 100
Total score: 5000 + 4342 + ... + 100 = weighted sum
Why exponential decay?
- Recent popularity matters more than historical
- Trending queries rise fast, old fads decay
- Tuning λ controls memory of the systemTrending Query Detection
Trending Query DetectionExcalidraw diagram · editable shapes · reveal step by stepExplore
Trie Building Pipeline (Offline)
Batch Trie Construction
Batch Trie ConstructionExcalidraw diagram · editable shapes · reveal step by stepExplore
Trie Serialization Format
Trie Serialization FormatExcalidraw diagram · editable shapes · reveal step by stepExplore
Performance & caching
Caching Strategy
Multi-Layer Cache Architecture
Multi-Layer Cache ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Cache Invalidation
Problem: When trie is updated, cached suggestions become stale.
Strategy: TTL-based expiration (no active invalidation)
Justification:
- Suggestions change slowly (hourly trie rebuilds)
- Slightly stale suggestions are acceptable
- Active invalidation at CDN scale is expensive and complex
- 5-15 minute TTL = max staleness of 15 minutes
Exception: TRENDING queries
- Bypass CDN cache for trending prefixes
- Set Cache-Control: no-cache for trending-eligible short prefixes
- Or: use websocket push to update client cache for trendingAdvanced design
Sharding & Replication
Sharding Strategy
Sharding StrategyExcalidraw diagram · editable shapes · reveal step by stepExplore
Replication
ReplicationExcalidraw diagram · editable shapes · reveal step by stepExplore
Full Serving Topology
Full Serving TopologyExcalidraw diagram · editable shapes · reveal step by stepExplore
Personalization
User History Blending
User History BlendingExcalidraw diagram · editable shapes · reveal step by stepExplore
User History Storage
User History StorageExcalidraw diagram · editable shapes · reveal step by stepExplore
Offensive Content Filtering
Multi-Layer Filtering
Multi-Layer FilteringExcalidraw diagram · editable shapes · reveal step by stepExplore
Multi-Language Support
Language-Specific Challenges
Language-Specific ChallengesExcalidraw diagram · editable shapes · reveal step by stepExplore
Alternative Data Structures
Trie vs. Alternatives Comparison
| Data Structure | Prefix Lookup | Memory | Build Time | Update | Best For |
|---|---|---|---|---|---|
| Trie (compressed) | O(L) | Medium (30 GB) | Hours | Hard (rebuild) | Standard choice |
| Sorted array + binary search | O(L × log N) | Low (10 GB) | Fast | Hard | Small corpus |
| Hash map (prefix → suggestions) | O(1) amortized | High (100 GB+) | Fast | Easy | High QPS, small corpus |
| Ternary search tree | O(L) | Low (20 GB) | Hours | Medium | Memory-constrained |
| DAWG (directed acyclic word graph) | O(L) | Very low (5 GB) | Very slow | Very hard | Read-heavy, static corpus |
| Inverted index (prefix → docs) | O(1) | Medium | Fast | Easy | When mixing with search |
Approach: Precomputed Hash Map (for high-QPS systems)
Alternative to trie for extreme QPS requirements:
Precompute ALL possible prefixes → top-K map:
"a" → [apple, amazon, apple pie, ...]
"ap" → [apple, app store, apple pie, ...]
"app" → [apple, app store, apple pie, ...]
"appl" → [apple, apple pie, apple music, ...]
"apple" → [apple, apple pie, apple music, ...]
...
Storage: 500M queries × avg 20 chars = 10B prefix entries
10B × (key + 10 suggestion IDs) = ~200 GB
TOO MUCH for in-memory!
Optimization: Only precompute prefixes of length 1-6
(covers ~95% of user typing before they click a suggestion)
26^1 + 26^2 + ... + 26^6 ≈ 300M prefix entries
But with actual query distribution: ~50M meaningful prefixes
50M × 500 bytes = ~25 GB ← feasible!
For prefixes > 6 chars: fall back to trie lookupApproach: Inverted Index Style (Elasticsearch)
Instead of a trie, use an inverted index with edge n-grams:
Document: "apple pie recipe"
Index terms (edge n-grams):
"a", "ap", "app", "appl", "apple"
"p", "pi", "pie"
"r", "re", "rec", "reci", "recip", "recipe"
Query: prefix "app" → lookup posting list for "app"
→ returns all suggestions containing a word starting with "app"
Advantages:
- Leverages existing search infrastructure (Elasticsearch)
- Easy updates (just index new documents)
- Built-in relevance scoring, filtering, aggregations
Disadvantages:
- Higher latency than in-memory trie (~5-10ms vs ~1ms)
- More complex query for phrase completion
- Overkill for simple prefix matching
Best for: Medium-scale systems (< 10K QPS) that already use ElasticsearchReal-Time Updates (Streaming Path)
Near Real-Time Trie Update Architecture
Near Real-Time Trie Update ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Advanced Topics
Fuzzy Matching (Typo Tolerance)
Problem: User types "aple" but means "apple"
Approach 1: Edit Distance at Query Time
For prefix "aple", find all trie nodes within edit distance 1:
"aple" → "apple" (insert 'p')
"aple" → "able" (substitute p→b)
"aple" → "ape" (delete l)
Cost: Expensive! For each prefix, explore O(26^d) branches
where d = max edit distance
Optimization: BK-tree or Levenshtein automaton
- Precompute finite automaton for edit distance ≤ 2
- Intersect automaton with trie in single traversal
- O(L) time regardless of edit distance
Approach 2: Phonetic Matching
Soundex / Metaphone encoding:
"aple" → A140
"apple" → A140 (same code!)
Store phonetic codes in separate index
Match phonetically, then boost exact prefix matches
Approach 3: Query Log Corrections (most practical)
When user types "aple" and then reformulates to "apple":
Store mapping: "aple" → "apple" with confidence score
At query time: lookup "aple" in correction map
If found: also search for "apple" and merge results
Low latency: O(1) hash map lookup
High quality: based on actual user behaviorContext-Aware Suggestions
Beyond prefix matching — use context for better suggestions:
1. Previous query context:
Query 1: "python"
Query 2: "for" → suggest "for loop python", not "ford trucks"
2. Page context:
User is on a cooking website
Types "ch" → suggest "chicken recipe", not "chrome download"
3. Time context:
During NFL season: "sup" → "super bowl" boosted
Tax season: "h" → "h&r block", "how to file taxes" boosted
4. Device context:
Mobile: shorter suggestions (screen space limited)
Desktop: longer, more detailed suggestions
Implementation:
Context vector + suggestion scoring:
score(suggestion) = base_score
× context_boost(query_history)
× time_boost(current_events)
× device_factor(screen_size)Zero-Prefix Suggestions
When user clicks search box but hasn't typed anything:
Show:
1. User's recent searches (from personalization store)
2. Currently trending queries
3. Seasonal / contextual suggestions
Implementation:
- No trie lookup needed
- Precomputed list per user segment
- Cached aggressively (one request per search session)
- Updated every 15 minutes
This is the "prefetch" request — sent on search box focusEdge cases
Workshop note · added for the website’s common reading format
Walk through three failure moments in Typeahead & Autocomplete: a request times out before its result is known; a dependency becomes slow rather than unavailable; and a process restarts after committing state but before acknowledging it.
For each case, name the authoritative record, define a safe retry, cap resource usage, and describe what the caller sees. Quality monitoring should help detect and contain the problem: Measure p99 response time, empty suggestions, and freshness after each index release.
Tradeoffs
Key Tradeoffs
Pre-computed Top-K vs. On-the-Fly Ranking
Pre-computed (chosen for global suggestions):
+ O(L) lookup — walk trie, read pre-stored top-K
+ Extremely fast (< 1 ms)
+ Simple serving logic
- Stale until trie rebuild (hourly)
- Can't easily incorporate real-time personalization into top-K
- Rebuild cost: hours of compute for 500M queries
On-the-fly ranking:
+ Always fresh
+ Easy to blend personalization, trending, context
- Slower: must traverse subtree, score, sort at query time
- For popular short prefixes, subtree can have millions of entries
- Latency unpredictable (depends on subtree size)
Hybrid approach (Google's / production systems):
Pre-compute top-K for global suggestions → fast base
Merge with personalization + trending at query time → fresh blend
Total cost: O(L) trie walk + O(K) merge = still very fastTrie Rebuild Frequency
Frequent rebuilds (every 15 min):
+ Fresher suggestions
+ Trending queries appear faster
- Higher compute cost
- More deployment risk (more frequent swaps)
- Diminishing returns (most queries don't change in 15 min)
Infrequent rebuilds (daily):
+ Lower compute cost
+ Stable, well-tested suggestions
- Stale: trending topics delayed by hours
- New queries don't appear until next rebuild
Chosen: Hourly batch rebuild + real-time overlay
- Best of both: stable base + fresh trending
- Overlay is small (100K-1M entries) — cheap to update
- Merge at query time is O(K) — negligible costGlobal vs. Regional Tries
Single global trie:
+ Simpler architecture
+ One build pipeline
+ Global trends visible everywhere
- "pizza near me" suggestions are same in Tokyo and New York
- Wastes memory on irrelevant suggestions per region
Per-region/locale trie:
+ Region-specific suggestions ("weather tokyo" boosted in Japan)
+ Smaller per-region trie (less memory)
+ Better relevance for location-dependent queries
- More complex build pipeline (one per locale)
- ~50 languages × ~10 regions = 500 tries to maintain
- Trending detection harder (smaller per-region signal)
Hybrid:
- Global trie for universal queries (brands, tech terms)
- Regional overlay for location-specific queries
- Merge at query time based on user's locale/locationMemory vs. Disk for Trie Storage
| Factor | In-Memory Trie | Disk-Backed (mmap) |
|---|---|---|
| Lookup latency | < 0.1 ms | 0.1-1 ms (cold), < 0.1 ms (warm) |
| Memory required | 50 GB per server | 4-8 GB (OS page cache) |
| Server cost | Higher (need large RAM) | Lower (smaller instances) |
| Cold start time | Slow (load 50 GB) | Fast (mmap, load on demand) |
| Suitable for | High QPS (> 10K/server) | Moderate QPS (< 5K/server) |
Chosen: Memory-mapped (mmap) trie file
- OS handles paging — hot paths stay in RAM
- Cold paths (rare prefixes) loaded on demand from SSD
- Best cost/performance ratio at scale
- Cold start: instant (just mmap the file, no deserialization)
Precision vs. Recall in Suggestions
High precision (fewer, more relevant suggestions):
+ Users trust suggestions more
+ Less visual noise
+ Lower risk of offensive/irrelevant suggestions
- May miss niche queries user actually wants
- Less "discovery" of new queries
High recall (more diverse suggestions):
+ Users discover queries they didn't think of
+ Better for exploratory search
- More irrelevant suggestions → user ignores autocomplete
- Higher risk of showing offensive content
- More visual noise
Chosen: High precision (K=5-8 suggestions)
- Quality over quantity
- Users want THE suggestion they're looking for
- Offensive content risk minimized with fewer suggestions
- Can show "more suggestions" button for users who want recallReliability & fault tolerance
Fault Tolerance & Reliability
Failure Scenarios & Handling
| Failure | Impact | Mitigation |
|---|---|---|
| Single suggestion server dies | One replica unavailable | Load balancer routes to other replicas; auto-restart |
| Trie build pipeline fails | Stale trie (hours old) | Serve previous trie version; alert on build failure |
| Trending pipeline lag | Missing trending suggestions | Gracefully degrade: serve without trending boost |
| Personalization (Redis) down | No personalized suggestions | Serve global-only suggestions; no user-visible error |
| CDN outage | Higher load on origin | Origin servers absorb load; scale up if needed |
| All replicas of a shard down | Prefix range unavailable | Return empty suggestions for that range; partial degradation |
| Kafka lag (query log delay) | Delayed frequency updates | Trie still serves from last build; trending may lag |
Health Checking & Monitoring
Health Checking & MonitoringExcalidraw diagram · editable shapes · reveal step by stepExplore
Production architecture
Serving Infrastructure
Global Deployment
Global DeploymentExcalidraw diagram · editable shapes · reveal step by stepExplore
Latency Budget
Latency BudgetExcalidraw diagram · editable shapes · reveal step by stepExplore
Further exploration
Workshop note · added for the website’s common reading format
Rebuild Typeahead & Autocomplete from memory, then change one assumption: ten times more traffic, a new region, or a stricter consistency requirement. Which component must change first—and which does not?
Compare Suggestion service with the same boundary in a related design. Write down one alternative you rejected, what it would simplify, and when you would choose it instead. Follow the source link at the end of this article to explore the original document.
Interview playbook
System Design Interview Tips
What Interviewers Look For
- Trie fundamentals — can you explain how a trie works and why it's the right data structure?
- Scale reasoning — how much data, how many requests, how much memory?
- Top-K at each node — this is the key insight that makes prefix lookup O(L)
- Data pipeline — how do you collect query frequencies and build the trie?
- Caching strategy — multi-layer caching is critical for 100K+ QPS
- Real-time trending — how do you surface breaking news quickly?
- Tradeoff reasoning — rebuild frequency, precision vs recall, global vs regional
Common Follow-Up Questions
| Question | Key Points |
|---|---|
| "How do you handle a new trending query?" | Real-time overlay via streaming pipeline; merge with base trie at query time |
| "What if the trie doesn't fit in memory?" | Shard by prefix range; mmap for disk-backed; keep only top-N queries |
| "How do you personalize without storing user data?" | Client-side history in localStorage; send with request; no server storage |
| "How do you prevent offensive suggestions?" | Multi-layer: blocklist → regex → ML classifier → human review for trending |
| "What happens if a user types very fast?" | Client-side debouncing + request cancellation; prefix reuse for local filtering |
| "How do you update the trie without downtime?" | Blue/green deployment: build new trie → load into standby → swap pointer |
Suggested 45-Minute Interview Structure
0-5 min: Clarify requirements (search suggestions? K=? latency?)
5-10 min: Scale estimations (QPS, corpus size, memory)
10-18 min: Core data structure (trie, compressed trie, top-K precomputation)
18-28 min: System architecture (serving path, data pipeline, caching)
28-35 min: Deep dive: pick ONE
- Option A: Real-time trending pipeline
- Option B: Sharding & replication
- Option C: Personalization strategy
35-42 min: Tradeoffs (rebuild frequency, trie vs alternatives, global vs regional)
42-45 min: Monitoring, failure handling, fuzzy matching