Technologies referenced in this case study: Redis · Elasticsearch · Kafka · Flink
Related case studies: Search Indexing · CDN & Edge Caching · Distributed Caching · Leaderboards & Counting · Stream Processing
How to Use This Case Study#
Organized for interview use first, reference second. Full-text retrieval, inverted indexes and relevance scoring live in Search Indexing; this case study is about answering a prefix in under 100ms, millions of times a second, without ever suggesting something you'll regret.
| Mode | Time | What to Read |
|---|---|---|
| Quick Review | 15 min | Executive Summary → Interview Walkthrough → Fault Lines table → Drills 1–3 |
| Targeted Study | 1–2 hrs | Executive Summary → Walkthrough → Section 3 (Fault Lines) → Section 4 (Failures) → Deep Dives 1, 2 and 4 |
| Deep Dive | 3+ hrs | Everything, including the Principal Lens (Section 11) and the appendices |
What is Search Autocomplete? — Why interviewers pick this topic
Typeahead returns a short ranked list of completions as the user types: "how to" → "how to tie a tie", "how to screenshot on mac", … It fires on nearly every keystroke, so it is usually the highest-QPS read endpoint a search product owns — often 5–10× the QPS of search itself — and it is the most visible: suggestions appear before the user has asked for anything.
Before vs After — the offensive suggestion incident:
Without a safety design:
t=0: A coordinated group searches "<public figure> is a <slur>" 40K times
t=+1h: Hourly pipeline promotes it into the top-5 for "<public figure> is"
t=+1h: Prefix tables rebuilt and pushed to all serving nodes
t=+3h: Screenshot trends on social media
t=+4h: Engineer manually edits the blocklist file; full rebuild needed
t=+6h: Rebuild + rollout completes. Press coverage already written.
With a safety design:
t=0: Same coordinated burst
t=+1h: Pipeline requires ≥N distinct users over ≥M days; burst fails threshold
t=+1h: Velocity anomaly flags the pattern for trust-and-safety review
t=+20min: (If anything slipped through) T&S adds a serving-time override
t=+21min: Override propagates to all nodes and edge caches purge — no rebuild
Why interviewers reach for this question: the textbook answer (a trie with top-K at each node) fits on a whiteboard in five minutes. Everything that decides your level comes after that: the per-keystroke latency budget, where caching happens, how fresh suggestions get, who owns the blocklist, and how to suggest in Japanese where users type phonetic input that isn't the final text.
Mechanics Refresher: Data Structures
| Structure | How It Works | Pros | Cons |
|---|---|---|---|
| Trie, walk + DFS at query time | Walk to prefix node, traverse subtree to find top-K | Simple; supports any K | Subtree traversal for short prefixes touches millions of nodes — unbounded latency |
| Trie with top-K cached per node | Each node stores its precomputed top-K completions | O(prefix length) lookup | Pointer-heavy memory (~10× the raw strings); rebuild to update rankings |
| Prefix table (hash map prefix → top-K) | Precompute top-K for every prefix up to length L; store in KV | O(1) lookup; trivially shardable and cacheable; any KV store works | Storage grows with Σ(query length); long-tail prefixes need a fallback |
| FST (finite state transducer) | Compressed automaton sharing prefixes and suffixes; weights on arcs | 5–10× smaller than a trie; Lucene / Elasticsearch completion suggester use it | Immutable — rebuild to change; harder to update incrementally |
| Search index with edge n-grams | Index every prefix of every term in an inverted index | Handles mid-string and multi-term matching | 10–100× slower per request than a lookup; overkill for query completion |
For most production systems: precomputed top-K per prefix, stored as an immutable, versioned prefix table (or FST) held in memory, with a small real-time overlay for trending and a serving-time override layer for safety. The data structure is never the interview question.
Executive Summary
If you only read one section, read this. Typeahead is a read-optimized precomputation problem with a safety obligation attached.
What This Interview Actually Tests#
Typeahead is not a trie question. Everyone can draw a trie.
This is a latency-budget and precomputation question that tests:
- Whether you derive the design from a per-keystroke budget (~100ms end to end, ~10–20ms server)
- Whether you move work from query time to build time — and know what that costs in freshness
- Whether you place caching at every layer (client, edge, server) and know which layer absorbs what share of traffic
- Whether you treat suggestions as published content with safety, legal and privacy obligations
The key insight: every suggestion is something your product said to the user before they asked. Staff engineers design typeahead as a publishing pipeline — build offline, review, publish versioned, override instantly — with a serving tier that is just a fast lookup.
The L5 → L6 → L7 Contrast — Start Here#
| Behavior | Senior (L5) | Staff (L6) | Principal (L7) |
|---|---|---|---|
| First move | Draws a trie with top-K per node | Asks "query completion, entity lookup, or in-app navigation?" and derives a per-keystroke latency budget | Asks which surfaces already run their own suggesters (search box, maps, shopping, help center) and who owns suggestion safety across them |
| Data structure | Trie in memory, updated on every search | Precomputed top-K per prefix, immutable versioned snapshot, built offline; tiny real-time overlay for trending | Treats the suggestion index as a shared artifact with a build/publish contract; serving is commodity |
| Freshness | "Update the trie in real time" | Daily/hourly batch for the base, 5-minute streaming overlay for trending, with velocity-anomaly gating | Sets freshness tiers by surface (news: minutes, commerce: hours) and prices each tier against the abuse surface it opens |
| Safety | "Filter bad words" | Blocklists + k-anonymity thresholds at build time; serving-time override with minutes-level propagation; T&S owns policy | Writes the org's suggestion policy with legal and T&S; defines removal SLA, appeals, and audit; one override service for every surface |
| Scale | "Shard the trie by first letter" | Replicate the whole index if it fits (tens of GB); edge-cache short prefixes; shard only the long tail | Decides what lives on the client, the CDN and the origin based on cost per million keystrokes |
Why "first move" separates levels
L5: Starts from the data structure because that's what the prompt names. The result is a design with no stated latency budget and no notion of who the suggestions are for.
L6: "Query completion for a web-scale search box, entity typeahead for people and pages, and command completion inside an app are three different systems. The first is popularity-ranked and cacheable; the second is personalized and graph-ranked; the third is small enough to ship to the client. I'll design query completion and show where personalization plugs in. Budget: 100ms keystroke-to-render, so 10–20ms server p99."
L7: "How many suggesters does the company run today? If search, maps and shopping each have one, they each have their own blocklist — and the next safety incident will come from the one with the weakest list. I'd design the shared build-and-safety pipeline and let each surface own ranking."
Why "freshness" separates levels
L5: Proposes updating counts in the trie on every search. That puts writes on a structure serving a million reads a second and — more importantly — lets any burst of searches publish itself immediately.
L6: Splits freshness into tiers. The base index rebuilds daily (or hourly) from aggregated logs with time decay. A streaming job detects trending queries in 5-minute windows and publishes a small overlay, gated by distinct-user thresholds and velocity checks. Serving merges base + overlay. Fresh enough for breaking news, not so fresh that a brigade can publish itself.
L7: Recognizes that freshness is abuse surface. Every minute shaved off trending latency is a minute less for review. Sets freshness per surface with T&S: news may get 5-minute trending with human-in-the-loop for sensitive entities; product search gets hourly with no human review.
Why "safety" separates levels
L5: A profanity list applied at build time.
L6: Four layers: (1) privacy thresholds — a query must be issued by ≥N distinct users to be suggestible, so private searches never leak; (2) policy classifiers and blocklists at build time; (3) a serving-time override (deny by exact string, by prefix+completion pair, by entity) that propagates in minutes without a rebuild; (4) edge-cache purge for affected prefixes. Trust & Safety owns the policy; search infra owns the mechanism and the propagation SLA.
L7: Turns it into governance: a published suggestion policy, a legal-removal workflow with an SLA, an audit log of every override, and a shared override service for all surfaces. Measures "time to removal" as an org SLO.
The Staff Positions#
| Position | Rationale |
|---|---|
| Precompute top-K per prefix offline | Moves all ranking work out of the 10–20ms serving budget; serving becomes a lookup |
| Immutable, versioned index snapshots | Atomic swap, instant rollback, reproducible debugging; no in-place mutation at 1M QPS |
| Base + trending overlay + override layer | Three update speeds for three needs: quality (daily), freshness (minutes), safety (seconds) |
| Cache at the client and the edge first | 50–70% of keystroke requests never need to reach origin |
| Replicate before you shard | A filtered prefix table is tens of GB — it fits in RAM; sharding adds fan-out and hot-shard problems for little benefit |
| k-anonymity threshold before anything is suggestible | Suggestions are published content; a query seen from 3 users is someone's private search |
| Personalization as a re-rank, not a separate index | Keep the global index cacheable; blend a small per-user candidate set at serve time |
The Three Intents#
| Intent | Constraint | Strategy | Failure Mode | Correctness Bar |
|---|---|---|---|---|
| Query completion (web/product search) | Massive QPS, global popularity, cacheable | Precomputed top-K per prefix; edge-cached; trending overlay | Offensive or stale suggestions; latency spikes | Relevance measured by suggestion CTR and keystrokes saved; zero policy-violating suggestions |
| Entity typeahead (people, pages, places) | Personalized, graph-dependent, privacy-sensitive | Per-user candidate set (friends, recent) + global entity index; merge and rank | Leaking private entities; missing the obvious friend | Recall of the intended entity in top-3 |
| In-app navigation / command completion | Small corpus (10K–1M items), per-tenant permissions | Ship index to client or query per-tenant index; fuzzy matching | Showing items user can't access | Permission-correct; typo tolerant |
🎯 Staff Move: "I'll design query completion for the main search box — that's where the QPS, the caching and the safety risk live. Entity typeahead reuses the serving tier with a per-user candidate set merged in, and I'll show where that plugs in."
The Five Fault Lines#
| # | Fault Line | The Tension |
|---|---|---|
| 1 | Precompute vs Compute at Query Time | Lookup in 1ms with fixed rankings, or rank on demand with flexibility and 10–50× the cost? |
| 2 | Freshness vs Safety & Stability | Surface trending queries in minutes, or gate them for abuse and quality? |
| 3 | Global Cacheable vs Personalized | One answer per prefix (cache everywhere) or per-user answers (cache nowhere)? |
| 4 | Replicate vs Shard | Full copy on every node (simple, no fan-out) or partition by prefix (scales memory, hot shards)? |
| 5 | Central Policy vs Product Autonomy | One suggestion policy and override service, or each surface curates its own? |
In the Wild: Real Production Systems#
Why this section belongs here: Typeahead is a well-documented problem with public engineering write-ups. Citing them shows you know the hard parts are latency and safety, not tries.
Facebook — Typeahead for people and pages#
Facebook's engineering blog post "The life of a typeahead query" (2010) described a design that pre-loads a user's first-degree connections and frequently-used entities to the browser, so many queries are answered client-side before any request is sent. Server-side, queries fan out to a global index of entities and a per-user graph index, results are merged and ranked by social signals, all within a tight budget measured in tens of milliseconds.
Staff insight: The fastest request is the one you don't make. Pushing a per-user candidate set to the client is a caching decision driven by the latency budget — and it's the canonical example of personalization as a merge, not a separate global index.
LinkedIn — Cleo#
LinkedIn open-sourced Cleo (2012), a typeahead library used for its search boxes. It partitions a large entity set, uses compact per-element prefix signatures (Bloom-filter style) to cheaply reject non-matching candidates, and then scores the survivors — supporting both "network" (personal connections) and "generic" (global) typeahead.
Staff insight: Separating a cheap candidate filter from an expensive scorer is the same move as top-K precomputation: bound the work per keystroke so latency doesn't depend on corpus size.
Google — Autocomplete policies#
Google publishes autocomplete policies describing categories of predictions it removes (e.g., sexually explicit, hateful, violent, dangerous content, and certain predictions about individuals), and gives users a way to report predictions. Removals are driven by automated systems plus enforcement teams.
Staff insight: At scale, suggestion safety is a published policy with an enforcement pipeline, not a word list. In an interview, naming the policy owner and the removal path is a stronger signal than any data structure.
Elasticsearch — Completion suggester#
Elasticsearch's completion suggester builds an in-memory FST per segment for prefix lookups, trading index-time cost and immutability for very fast reads. It supports weights and contexts (e.g., category or geo filters) but not arbitrary ranking at query time.
Staff insight: Even a general-purpose search engine ships a precomputed, immutable structure for autocomplete — because query-time full-text search can't meet the per-keystroke budget.
What Interviewers Probe#
| After You Say... | They Will Ask... | (What They're Evaluating) |
|---|---|---|
| "Trie with top-K at each node" | "How much memory? How do you update rankings without locking a structure serving 1M QPS?" | Do you know the build/serve split? |
| "We'll update counts on every search" | "A brigade searches an offensive phrase 50K times. What happens?" | Freshness as abuse surface |
| "Cache in Redis" | "What's the hit rate for 'a'? For 'how to tie a bow ti'? Where's the cache closest to the user?" | Layered caching and the prefix-length distribution |
| "Shard by first letter" | "What's the QPS on the 's' shard vs the 'x' shard?" | Hot shards; replicate-vs-shard judgment |
| "Personalize by user history" | "Now nothing is cacheable. What's your latency and cost?" | Personalization as re-rank |
| "Filter bad words" | "Legal sends a court order to remove a suggestion. How long until it's gone everywhere?" | Override layer and propagation SLA |
System Architecture Overview#
Reading the diagram: The client and edge absorb most keystrokes. The serving tier is a dumb, fast lookup over an in-memory snapshot plus a small trending overlay, filtered through an override list on every response. All ranking, privacy filtering and policy classification happen offline in the build pipeline. Trust & Safety has a direct path to the override filter and the CDN purge — removals never wait for a rebuild.
Quick-Reference: The 30-Second Cheat Sheet#
| Topic | The L5 Answer | The L6 Answer — Say This | The L7 Answer — Say This |
|---|---|---|---|
| Data structure | "Trie with top-K per node" | "Precomputed top-K per prefix, immutable snapshot, atomic swap. The structure is an implementation detail." | "The index is a published artifact with a build contract; any surface can serve it." |
| Latency | "It's in memory, so it's fast" | "100ms keystroke-to-render; 10–20ms server p99; debounce 50–100ms; client and edge serve most of it." | "We pay per keystroke — I'd price client vs edge vs origin per million keystrokes." |
| Freshness | "Update on every search" | "Daily base, hourly refresh, 5-min trending overlay gated by distinct users and velocity." | "Freshness is abuse surface; T&S sets the trending tier per surface." |
| Personalization | "Per-user trie" | "Global top-K plus a per-user candidate set merged at serve time; keeps the global response cacheable." | "Personalization is opt-in per surface and has a privacy review — it changes what we store about users." |
| Safety | "Profanity filter" | "k-anonymity threshold, policy classifiers, serving-time overrides in minutes, edge purge." | "One override service, one policy, one audit log for every suggester; time-to-removal is an SLO." |
| Scale | "Shard by first letter" | "Replicate the full index — it's tens of GB. Shard only if it doesn't fit, by hash, with short prefixes replicated everywhere." | "Cost lives at the edge, not the index; the index is cheap, the keystrokes aren't." |
Key Numbers Worth Memorizing#
| Metric | Value | Why It Matters |
|---|---|---|
| Keystroke-to-render budget | ~100ms | Above ~100–150ms, suggestions feel laggy and lag behind typing |
| Inter-keystroke interval | ~100–300ms | Debounce at 50–100ms drops ~30–50% of requests without perceived lag |
| Server p99 budget | 10–20ms | What's left after RTT (20–80ms) and rendering |
| Suggest requests per search | ~4–8 after debounce | Typeahead QPS is several times search QPS |
| Example scale | 5B searches/day → ~30B suggest req/day → ~350K/s avg, ~1M/s peak | Drives every caching decision |
| Requests with prefix length ≤ 3 | ~30–40% | Short prefixes are few and hot — perfect for the edge |
| Client + edge hit rate | ~50–70% combined | Origin sees a fraction of keystrokes |
| Suggestible queries after filtering | ~10–100M | Min-count + k-anonymity cut billions of raw queries to this |
| Prefix table size (top-10, L ≤ 20) | ~20–80 GB | Fits in RAM on a large node — replicate rather than shard |
| k-anonymity threshold | ≥ 50–1,000 distinct users over ≥ 7–30 days | Prevents private queries from becoming suggestions |
| Trending overlay latency | 5–15 min | Breaking news needs minutes; faster increases brigade risk |
| Override propagation SLA | < 5–15 min to all nodes + edge | The real safety requirement |
Interview Walkthrough
The most common mistake: Candidates spend 20 minutes on trie node layouts and memory per pointer, then run out of time before latency budgets, caching layers, freshness and safety — which is where the level is decided. The phases below get the data structure done in 5 minutes.
Phase 1: Requirements & Framing (2–3 min)#
Functional requirements in 30 seconds:
"As the user types into the search box, return up to 10 ranked completions for the current prefix, per keystroke."
Then the framing that matters:
"There are three typeahead problems hiding here — query completion, entity lookup for people and pages, and in-app command completion — and they rank and cache differently. I'll design query completion for the main search box and show where personalization plugs in."
Then commit to numbers:
"I'll assume 5B searches a day, ~6 suggest requests per search after debouncing — about 350K requests/s average and 1M/s peak. Budget: 100ms from keystroke to rendered list, so the server gets 10–20ms p99. Suggestions must never include policy-violating or private queries, and removals must propagate within minutes."
🎯 Staff Move: Putting "removals must propagate within minutes" in the requirements tells the interviewer you see suggestions as published content. It also sets up the override layer, which is the piece most candidates never design.
Phase 2: Core Entities & API (1–2 min)#
- Suggestion:
text,score,source(base / trending / personal),entity_ref? - PrefixEntry:
(locale, normalized_prefix) → [top-K suggestion ids + scores] - IndexSnapshot:
version,locale,built_at,policy_version,checksum - Override:
match(exact / prefix+completion / entity / regex),action(deny / demote),reason,owner,expires_at
Serve path:
GET /v1/suggest?q=how+to+t&locale=en-US&client=web&session=…
200 OK Cache-Control: public, max-age=300 (if non-personalized)
{ "q": "how to t", "suggestions": [ {"text":"how to tie a tie","src":"base"}, … ],
"index_version": "en-US-2026-10-01T03", "personalized": false }
Override path (T&S, cold):
POST /v1/overrides { match: {type:"pair", prefix:"<name> is", completion:"<term>"},
action:"deny", reason:"policy:harassment", ticket:"TNS-4411" }
🎯 Staff Move: "The response says whether it's personalized. Non-personalized responses are public-cacheable at the edge; personalized ones are private. That single bit decides whether a keystroke costs us a CDN hit or an origin request."
Phase 3: High-Level Architecture (≤5 min)#
"Reads are a lookup in an in-memory prefix table — one hash get per request. All ranking happens in the offline build. Trending is a small overlay merged at serve time. Overrides are applied to every response. Client and edge caches take most of the traffic."
Phase 4: Transition to Depth#
"The lookup is the easy part. The design lives in four places: the per-keystroke latency budget and caching layers, how fresh suggestions get without letting brigades publish themselves, how personalization coexists with caching, and the safety pipeline. I'll start with the latency budget because it drives everything else."
Phase 5: Deep Dives (25–30 min)#
| Deep Dive | The 60-Second Version | Go Here |
|---|---|---|
| Latency budget & caching | 100ms total: RTT 20–80ms, server 10–20ms, render 10ms. Debounce 50–100ms. Client caches prefix results; edge caches non-personalized prefixes with 5–15 min TTL. ~50–70% never reach origin. | Fault Line 3, Appendix D |
| Index build | Logs → normalize → aggregate with exponential decay (half-life ~7 days) → k-anonymity filter → policy filter → top-K per prefix (L ≤ 20) → versioned snapshot → staged rollout. | Fault Line 1, Appendix A |
| Freshness & trending | 5-min streaming windows; trending score = current rate vs baseline; require distinct-user and velocity checks; overlay of ≤100K entries merged at serve. | Fault Line 2, Drill 4 |
| Personalization | Per-user recent queries (last ~100) in a KV store; serve-time merge with global top-K; personalized responses not edge-cacheable. | Fault Line 3, Drill 6 |
| Sharding & hot prefixes | Replicate full snapshot (tens of GB) on every node; if too large, hash-shard long prefixes and replicate short ones everywhere. | Fault Line 4, Section 4.2 |
| Safety | Build-time: thresholds, classifiers, blocklists. Serve-time: override list, minutes to propagate, edge purge. Audit everything. | Fault Line 5, Deep Dive 2 |
Phase 6: Wrap-Up (2–3 min)#
"To summarize: a read-only, in-memory prefix table built offline, served from replicated nodes behind client and edge caches, with a trending overlay for freshness and a serving-time override layer for safety. The server budget is 10–20ms and most keystrokes never reach it. Next I'd add per-locale tokenization for CJK and IME input, typo-tolerant fallback for long prefixes that miss the table, and an experimentation framework for ranking. What I wouldn't build on day one is a real-time mutable trie — the freshness it buys isn't worth the safety and operational risk."
Common Timing Mistakes#
| Mistake | Time Lost | Fix |
|---|---|---|
| Designing trie node memory layout | 5–10 min | Say "precomputed top-K per prefix, FST or hash table" and move on |
| Debating Redis vs Memcached for the cache | 5 min | The important caches are the client and the CDN |
| Building a real-time update path into the trie | 10 min | Batch base + trending overlay; explain why |
| Skipping safety until asked | Whole level | Put propagation SLA in requirements |
| Ignoring non-Latin scripts | Missed L6 signal | One sentence on normalization and IME in the wrap-up |
1. The Staff Lens#
1.1 Why This Problem Exists in Staff Interviews#
Typeahead is deceptively small. The functional spec is one sentence, and the textbook data structure takes five minutes. That leaves 40 minutes in which the interviewer learns whether you can reason from a latency budget, move work from read time to build time, cache at the right layers, and own something that is effectively a publishing system: every suggestion is a sentence your company says to millions of people, unprompted.
It also has an unusual shape: extreme read QPS (often the single highest-QPS endpoint in a search product), a tiny response, a mostly-static dataset, and a small but critical write path (overrides) that must propagate faster than anything else in the system.
1.2 The L5 → L6 → L7 Contrast — Visual#
The L5 path produces a working autocomplete. The gap is that its first incident — a brigade, a court order, a hot prefix, a Japanese launch — has no designed answer.
1.3 The Staff Question That Cuts Through Everything#
"If a suggestion has to disappear right now, how long does it take, and who can make it happen?"
If the answer is "a rebuild, a few hours, an engineer", the design is L5. If the answer is "under 10 minutes to every node and every edge cache, by a T&S operator through an audited console", the design is Staff-grade. Answering it forces the build/serve split, the override layer, the cache purge path and the ownership model all at once.
2. Problem Framing & Intent#
2.1 The Three Intents — Explained#
Query completion. Completes what the user is typing into a full query, ranked by how often that query is issued (with time decay), how often the suggestion gets clicked, and freshness. The same prefix gets the same answer for most users in a locale — which makes it cacheable at the edge. The risk is reputational and legal: offensive, defamatory or private suggestions. The owner is search infra (mechanism) with Trust & Safety (policy).
Entity typeahead. Completes to things — people, pages, products, places — usually with an icon and a link. Ranking is dominated by the user's relationship to the entity (friends, follows, recent visits) and by entity popularity. It's mostly personalized, so it's not edge-cacheable; latency comes from pushing candidate sets to the client and from fast per-user lookups. The risk is privacy: never suggesting a private entity or revealing who someone looked up.
In-app navigation / command completion. Completes to commands, settings, documents or records inside an application. The corpus is small (thousands to a million items) and permission-filtered per user or tenant. Often the right answer is to ship the index to the client, or to run a per-tenant search index with prefix matching and typo tolerance. The risk is showing items a user isn't permitted to see.
🎯 Staff Move: "Query completion is a global, cacheable publishing problem. Entity typeahead is a per-user retrieval problem. Command completion is a permissions problem. I'll design the first and show the merge point for the second."
2.2 When NOT to Build a Typeahead Service#
- Small corpus (< ~100K items). Ship the list to the client and filter locally. Zero server QPS, zero latency, no service to own.
- Low traffic search boxes. If the search box gets 10 queries/s, use your search engine's completion suggester (Elasticsearch, OpenSearch, Solr) — a separate service adds an on-call rotation for no benefit.
- Highly permissioned data with no shared ranking. If every user's visible corpus differs (e.g., enterprise documents), a shared prefix table doesn't help; use per-tenant search with prefix queries.
- When suggestions create more risk than value. Sensitive domains (health, legal, children's products) may choose curated suggestions only, or none.
🎯 Staff Move: "If the corpus is a few thousand help articles, I'd ship them to the client as a compressed list and filter in JavaScript. A suggest service is something you build because QPS or corpus size forces you to."
2.3 What the Interviewer Leaves Underspecified#
| Underspecified | Why It Matters | What to Assume Out Loud |
|---|---|---|
| Which intent | Ranking, caching and risk differ | Query completion for a search box |
| QPS | Drives caching layers | 350K/s avg, 1M/s peak |
| Number of suggestions | Response size, cache key | 8–10 |
| Freshness | Trending pipeline complexity | Base daily/hourly; trending in 5–15 min |
| Personalization | Kills edge caching | Light: recent queries merged at serve time |
| Locales | Normalization, tokenization, IME | 40+ locales; separate index per locale |
| Matching semantics | Prefix only? Mid-word? Typos? | Prefix of normalized query; typo fallback for long prefixes |
| Safety obligations | Override layer, SLA | Policy removal within minutes; legal removals audited |
2.4 Precise Terminology#
| Term | Precise Meaning |
|---|---|
| Prefix | The normalized text typed so far (case-folded, Unicode-normalized, whitespace-collapsed). |
| Completion / candidate | A full query string that starts with the prefix and is eligible to be suggested. |
| Top-K | The K highest-scored candidates for a prefix, precomputed at build time. |
| Snapshot | An immutable, versioned build of the prefix table for one locale. |
| Overlay | A small, frequently updated set of candidates (trending) merged with the base at serve time. |
| Override | A serving-time rule that removes or demotes a suggestion, applied after lookup. |
| k-anonymity threshold | Minimum number of distinct users who must have issued a query before it can be suggested. |
| Debounce | Client-side delay after a keystroke before sending a request; a newer keystroke cancels the pending one. |
| Keystrokes saved | Characters the user didn't have to type because they accepted a suggestion — a primary quality metric. |
| IME | Input Method Editor — software that converts phonetic input (e.g., pinyin, romaji) into characters; the text in the box may be pre-composition. |
3. The Five Fault Lines#
3.1 Fault Line 1: Precompute vs Compute at Query Time#
The tension: Precomputing top-K for every prefix makes serving a single lookup, but rankings are frozen until the next build and storage grows with the total length of all queries. Computing at query time (walk a trie, score candidates, rank) is flexible and always current, but latency scales with the subtree size — and for a 1-character prefix that subtree is the whole corpus.
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Query-time DFS over a trie | Always fresh; any K | Prefix "s" touches millions of nodes; p99 explodes | Users (lag), on-call (latency pages) |
| Query-time search with edge n-grams | Flexible matching, mid-word, typos | 10–50ms per query in a search engine; 10–50× the cost at 1M QPS | Infra budget |
| Precomputed top-K per prefix (Staff default) | ~1ms lookup; trivially cacheable; immutable snapshots roll back instantly | Rankings refresh only per build; long prefixes beyond L need a fallback | Build pipeline team owns freshness |
| Precomputed + query-time re-rank of top-N | Lookup top-50, re-rank with context (personal, locale, device) | Small extra CPU; must keep N small | Ranking team owns the re-ranker |
Staff default: Precompute top-K (K = 10, store N = 20–50 for re-ranking headroom) for every prefix up to length L = 20 characters of the normalized query. For prefixes longer than L, take the length-L entry and filter its candidates by the full prefix; if empty, fall back to a small query-time search over a long-tail index — rare (<2% of requests) and allowed a 50ms budget.
Sizing: 50M suggestible queries × average 25 chars, but prefixes are shared heavily. Distinct prefixes ≈ 200–500M. Each entry: 20 candidate IDs × 4 bytes + scores ≈ 120–160 bytes + key ≈ 200 bytes → ~40–100 GB raw; with an FST or front-coded keys, ~20–50 GB. Pruning prefixes nobody types (no traffic in 30 days) typically cuts another 30–60%.
🎯 Staff Move: "I'll move all ranking to build time. The server's job is one hash lookup and an override filter — that's how I hit a 10ms p99 at a million QPS on boring hardware."
3.2 Fault Line 2: Freshness vs Safety & Stability#
The tension: Users expect breaking news and new products to show up in suggestions within minutes. But every shortcut from "people searched it" to "we suggest it" is a path for brigading, spam and accidental publication of something harmful — and every refresh is a chance to roll out a bad index.
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Real-time counts update the live index | Freshest possible | Any coordinated burst publishes itself; no review; write contention on the read path | T&S (incidents), PR, users |
| Daily rebuild only | Stable, reviewable, cheap | Breaking news invisible for up to 24h | Users (stale), product (looks outdated) |
| Daily base + hourly refresh + 5–15 min trending overlay with gates (Staff default) | Fresh for real trends; brigades fail distinct-user and velocity gates; overlay is small enough to review | Gating delays genuine trends by minutes; two code paths | Search infra owns the overlay; T&S owns gate thresholds |
Staff default: Base index rebuilt daily (hourly for high-churn locales), with exponential time decay (half-life ~7 days for base, ~1 day for news-heavy verticals). Trending stream computes per-query rate over 5-minute windows vs a 7-day baseline; a query enters the overlay only if rate / baseline > 5×, distinct users ≥ 500 in the window, distinct /24 networks ≥ 100, and it passes the same policy classifiers as the base. Overlay capped at ~100K entries per locale, TTL 6–24h unless promoted into the base by the next build.
When to deviate: News or sports surfaces may lower thresholds and add human review for named entities. Commerce surfaces can drop trending entirely and refresh hourly.
🎯 Staff Move: "Trending is where abuse enters. I'll gate it on distinct users and network diversity, not raw counts — 50K searches from 300 accounts is a brigade, not a trend."
3.3 Fault Line 3: Global Cacheable vs Personalized#
The tension: A global answer per (locale, prefix) is cacheable everywhere — client, CDN, server — and is cheap. A personalized answer is better for the user but uncacheable outside the client, multiplying origin QPS and latency.
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Fully global | 50–70% offloaded to client + edge; simplest | Ignores the user's own history; worse for entity-like queries | Users (relevance) |
| Fully personalized per request | Best relevance | No edge caching; origin QPS ×2–3; per-user state on hot path | Infra budget; latency |
| Global base + personal merge (Staff default) | Global part cacheable; small personal list merged | Need a merge point; personalized responses must be marked private | Ranking team owns merge logic |
Staff default: Two requests or one merged response. Option A (cheapest): the client fetches the global list from the edge and merges its own locally stored recent queries (last ~50–100) that match the prefix — zero server personalization. Option B: the server merges a per-user candidate list (KV lookup, ~1ms) with global top-N and re-ranks; response marked Cache-Control: private. Start with A; add B only where measured relevance gain justifies the origin cost.
Privacy: recent-query history is user data — retention limits, deletion on request, and never shown on shared devices without a signed-in user.
🎯 Staff Move: "I'd personalize on the client first. The user's own recent searches are already on their device; merging them locally keeps the global response public-cacheable and costs us nothing."
3.4 Fault Line 4: Replicate vs Shard#
The tension: Sharding the prefix table across nodes scales memory, but prefix traffic is extremely skewed — single letters and common starts take a large share — so range sharding creates hot shards, and hash sharding still concentrates the hottest keys. Full replication avoids fan-out and hot shards but requires the whole table in every node's memory.
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Range shard by first letter(s) | Easy to reason about | 's', 'c', 'a' shards take several times the load of 'x', 'q'; rebalancing is manual | On-call (hot shard pages) |
| Hash shard by full prefix | Even key distribution | Hot individual keys still hit one shard; every request is one shard but cluster needs routing | Infra team (router) |
| Full replication (Staff default when it fits) | No routing, no hot shards, any node serves any prefix; scale by adding replicas | Each node needs 20–50 GB RAM per locale set; snapshot distribution is heavier | Infra budget (memory) |
| Hybrid: replicate short prefixes, hash-shard long tail | Hot keys everywhere; long tail partitioned | Two-tier routing | Infra team |
Staff default: Replicate. A 64–128 GB node holds the major locales; small locales share nodes. Group locales into serving pools. Shard only when one locale's table exceeds a node's RAM, then use the hybrid: prefixes of length ≤ 4 replicated in every shard (a few hundred MB), longer prefixes hash-sharded.
Hot-prefix math: at 1M QPS peak with ~60% absorbed upstream, origin sees ~400K QPS. If prefix "a" alone is ~1% of origin traffic (4K QPS), any node can serve it from RAM in microseconds — it's only a problem if you sharded it onto one node.
3.5 Fault Line 5: Central Policy vs Product Autonomy#
The tension: A central suggestion policy and override service guarantees consistent safety across every suggester, but slows product teams that need domain-specific curation. Per-surface policies move fast and fit their domain, but the weakest surface determines the company's exposure.
| Strategy | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Each surface owns its blocklist | Fast, domain-specific | Inconsistent; the next incident comes from the weakest list; legal removals must be applied N times | T&S (N integrations), legal, PR |
| Central policy, central curation of everything | Consistent | T&S becomes a bottleneck for product-specific tuning | Product velocity |
| Central safety floor + surface-owned ranking/curation (Staff default) | One override service and policy floor; surfaces add stricter rules and own ranking | Requires an override API every surface integrates | Platform builds it once |
Staff default: One override service, one audit log, one propagation SLA. Every suggester calls it (or loads its compiled deny-set) on every response. Surfaces may add stricter local rules; they may not opt out of the floor.
4. Failure Modes & Operational Reality#
4.1 Bad Index Rollout — The Empty (or Wrong) Suggestions#
Scenario: A build bug (a normalization change) produces a snapshot where every prefix containing an apostrophe maps to nothing, and many accented prefixes map to the wrong candidates. It rolls out to all nodes at once.
t=0: New snapshot en-US-2026-10-01T03 swapped on all nodes
t=+1min: suggest.empty_response_rate jumps from 2% to 9%
t=+5min: Suggestion CTR drops 15%; search volume dips slightly
t=+20min: On-call notices dashboard; no alert fired (threshold 15%)
t=+25min: Rollback to previous snapshot (atomic swap, seconds)
Detection: suggest.empty_response_rate per locale (alert at 2× baseline); suggest.ctr canary vs control; build-time validation against a golden set of 10K prefixes.
Mitigation: Immutable snapshots make rollback a pointer swap. Staged rollout: 1 node → 5% → 50% → 100% with automatic comparison of empty-rate and CTR between versions at each stage.
Owner: Search infra (build + rollout). Golden set maintained jointly with ranking.
4.2 Hot Prefix / Cache Stampede After Purge#
Scenario: T&S purges all edge-cached prefixes starting with a celebrity's name after a removal. The name is trending; tens of thousands of users are typing it. Every keystroke misses the edge simultaneously.
t=0: Purge of 1,200 prefixes across the CDN
t=+1s: Origin QPS for those prefixes: 0 → 60K/s
t=+5s: Suggest nodes fine (RAM lookup), but the API gateway's
connection pools to the region saturate
t=+30s: p99 for all suggest traffic in region: 15ms → 400ms
Detection: edge.origin_requests spike; gateway.pool_utilization; suggest.p99 by region.
Mitigation: Edge request coalescing (one origin fetch per key per POP, others wait); purge as soft purge (serve stale while revalidating for non-safety purges); for safety purges, replace with the corrected response rather than delete (push-to-edge).
Owner: Edge/CDN team owns coalescing; search infra owns purge strategy.
4.3 Brigaded Suggestion — Coordinated Search Abuse#
Scenario: A coordinated group searches "
Detection: Trending gate rejects it (distinct users 900, but distinct /24 networks 40 and account age median 2 days). trending.rejected_by_gate{reason} spikes; T&S review queue receives it.
Mitigation: Gates on user and network diversity; account-age and reputation weighting in aggregation (new and low-reputation accounts count less); base-index aggregation over ≥7 days dilutes bursts.
Owner: T&S owns thresholds and review; search infra owns gate implementation.
4.4 Override Propagation Lag — The Removal That Didn't Stick#
Scenario: Legal requires removal of a defamatory suggestion. T&S adds the override. It's applied on nodes within 2 minutes — but mobile apps cache suggestion responses locally for 24h, and the CDN TTL is 15 minutes. Users keep seeing it.
t=0: Override created
t=+2min: All serving nodes filter it
t=+15min: Edge caches expire
t=+24h: Mobile client caches expire
Detection: Synthetic probes from multiple regions and app versions query the affected prefix and assert absence; override.propagation_seconds p99 per layer.
Mitigation: Push the override deny-set to clients too (small, compiled bloom/hash set, refreshed with app config every few minutes); client filters cached responses against it. Edge: targeted purge for affected prefixes. Client cache TTL ≤ 1h for suggestions.
Owner: Search infra owns the SLA across all layers, including clients; mobile team owns client-side filter.
4.5 Latency Regression — p99 Doubles After a Ranking Change#
Scenario: The ranking team adds a serve-time re-ranker with a feature lookup. p50 is fine; p99 goes from 12ms to 45ms due to KV tail latency.
Detection: suggest.latency_p99 per stage (lookup, re-rank, override, serialize); alert at 25ms.
Mitigation: Hard per-stage deadlines: if the re-ranker misses 5ms, return the base ranking. Hedged KV requests at p95.
Owner: Ranking team owns the re-ranker budget; search infra enforces the deadline.
4.6 Locale Normalization Bug — Turkish I#
Scenario: A generic lowercase function maps "İstanbul" incorrectly; Turkish users typing "is" don't see Istanbul-related suggestions, and some prefixes collide.
Detection: Per-locale golden sets; suggest.ctr and empty_rate broken down by locale.
Mitigation: Locale-aware case folding and normalization (NFKC + locale rules) shared between build and serve as one library.
Owner: Search infra (internationalization specialist), with locale QA.
4.7 Operational Reality Matrix#
| Failure | Detection Signal | Blast Radius | Mitigation | Owner |
|---|---|---|---|---|
| Bad snapshot | suggest.empty_response_rate, CTR canary | One locale, all users | Staged rollout, pointer-swap rollback | Search infra |
| Stampede after purge | edge.origin_requests spike, suggest.p99 | Region | Request coalescing, push-replace | Edge + search infra |
| Brigaded suggestion | trending.rejected_by_gate, T&S queue | Brand/person, PR | Diversity gates, reputation weighting | T&S + search infra |
| Override lag | Synthetic probes, override.propagation_seconds | Legal exposure | Client deny-set, targeted purge | Search infra + mobile |
| Latency regression | Per-stage p99 | All users | Stage deadlines, fallback ranking | Ranking + search infra |
| Normalization bug | Per-locale golden sets | One locale | Shared normalization library | Search infra i18n |
| Pipeline stalled (no new snapshot) | index.age_hours > 36 | Freshness only | Serve last good snapshot; alert | Search infra |
| Personal KV down | personal.error_rate | Personalized users | Serve global only | Search infra |
5. Evaluation Rubric#
5.1 Level-Based Signals#
| Dimension | Senior (L5) | Staff (L6) | Principal (L7) |
|---|---|---|---|
| Framing | One autocomplete | Three intents; picks one; latency budget per keystroke | Inventories suggesters across the org; shared safety pipeline |
| Data structure | Trie with top-K | Precomputed, immutable, versioned snapshots; fallback for long tail | Index as a published artifact with a build contract reused by surfaces |
| Caching | Redis in front of the service | Client + edge + server, with hit rates and the personalization bit | Prices keystrokes by layer; sets client caching policy org-wide |
| Freshness | Real-time updates | Base + overlay with diversity gates | Freshness tiers negotiated with T&S per surface |
| Safety | Profanity list | k-anonymity, classifiers, overrides with propagation SLA across all layers | Published policy, removal SLA as org SLO, audit, legal workflow |
| Operations | "Add replicas" | Staged rollout, golden sets, per-stage deadlines, named owners | Game days for removal propagation; error budget for suggestion quality |
5.2 Strong Hire Signals#
| Signal | What It Sounds Like |
|---|---|
| Derives from the budget | "100ms per keystroke means 10–20ms for the server. That rules out query-time ranking." |
| Moves work offline | "Ranking happens in the build. Serving is a hash lookup and a filter." |
| Layers caching | "Client and edge take most keystrokes; personalization decides which responses can be cached publicly." |
| Treats suggestions as published content | "A suggestion is something we say. It needs a k-anonymity threshold and a removal SLA." |
| Handles non-Latin input | "For Japanese, the box holds kana pre-conversion — I index readings, not just surface text." |
5.3 Lean No-Hire Signals#
| Signal | Why It Misses the Bar |
|---|---|
| Updates the live trie on every search | Write contention on the hottest read path and no safety gate |
| Shards by first letter without addressing skew | Predictable hot shards |
| No caching discussion beyond Redis | Misses where 50–70% of traffic should be absorbed |
| No answer to "remove this suggestion now" | Misses the most consequential operational requirement |
| Personalizes everything server-side without costing it | Destroys cacheability; multiplies origin load |
5.4 Common False Positives#
- Detailed trie memory layout ≠ typeahead design. Knowing pointer sizes doesn't show you can meet a latency budget or remove a suggestion in minutes.
- "Use Elasticsearch completion suggester" ≠ done. It's a fine engine choice; it doesn't answer freshness, safety, caching or personalization.
- Real-time freshness enthusiasm ≠ product judgment. Faster trending is not free; it's abuse surface.
- Knowing Unicode normalization forms ≠ multi-language design. IME, segmentation and transliteration are the hard parts.
6. Interview Flow & Pivots#
6.1 Typical 45-Minute Shape#
| Phase | Time | Goal |
|---|---|---|
| Framing | 0–4 min | Intent, QPS, latency budget, safety requirement |
| Entities + API | 4–6 min | Prefix entry, snapshot, override; personalization bit |
| Architecture | 6–11 min | Client → edge → serving → build + trending + overrides |
| Deep dive 1 | 11–19 min | Latency budget and caching layers |
| Deep dive 2 | 19–27 min | Build pipeline and freshness |
| Deep dive 3 | 27–35 min | Safety or personalization |
| Operations | 35–41 min | Rollout, failure matrix, owners |
| Wrap-up | 41–45 min | Multi-language, evolution |
6.2 How Interviewers Pivot — And What They're Testing#
| Pivot | What They're Testing | Strong Response |
|---|---|---|
| "Make it work for Chinese and Japanese" | i18n depth | Index by reading (pinyin, kana) and surface form; IME composition events; no whitespace tokenization |
| "Support typos" | Fallback design | Typo-tolerant only for prefixes ≥ 4 chars on a miss; edit distance 1 via precomputed deletes or FST Levenshtein automaton |
| "Show trending within 1 minute" | Freshness vs safety | Possible for curated entities; for open queries, minutes are the safety budget |
| "We need per-user suggestions" | Cacheability | Client-side merge first; server merge only where measured |
| "Cut serving cost by half" | Cost levers | Raise edge TTL for short prefixes, increase debounce, prune unused prefixes |
6.3 What to Deliberately Skip#
- Full-text retrieval and ranking for the search results page — reference Search Indexing
- Trie node memory layout beyond one sentence
- Load balancer details
- ML ranking model architecture — name features and the latency budget instead
6.4 Follow-Up Questions to Expect#
- "How big is the index, and does it fit in memory?"
- "How do you roll out a new index without a bad version reaching everyone?"
- "A suggestion must be removed in 10 minutes. Walk me through every layer."
- "How do trending queries get in, and what stops abuse?"
- "What's cached where, and what's the hit rate at each layer?"
- "How do you handle a prefix longer than your precomputed length?"
- "How do you evaluate whether a ranking change made suggestions better?"
7. Active Drills#
Drill 1: The Opening (Intent + Budget)#
Prompt: "Design autocomplete for our search box."
Staff Answer
"Before the data structure — which typeahead? Query completion, entity typeahead for people and pages, and in-app command completion rank, cache and fail differently. I'll design query completion for the main search box, and show where per-user entities merge in.
Scale: 5B searches/day, ~6 suggest requests per search after a 75ms debounce — ~350K requests/s average, ~1M/s peak. Budget: 100ms keystroke-to-render; RTT takes 20–80ms, rendering ~10ms, so the server gets 10–20ms p99. That budget rules out ranking at query time, so I'll precompute top-K per prefix offline and serve lookups from memory.
Non-functional I want to state now: suggestions are content we publish. Nothing becomes suggestible unless enough distinct users searched it, and any suggestion must be removable everywhere — including caches — within 10 minutes.
I'll go: budget and caching layers → build pipeline → freshness → safety → personalization → multi-language."
Why this is L6:
- Picks an intent and explains why the others differ
- Derives architecture from a latency budget with arithmetic
- Puts safety and removal SLA in the requirements, not in a follow-up
What L7 adds:
- Asks how many suggesters the company runs and whether a shared safety service exists — a new one with its own blocklist is a liability
- Names the owners up front: search infra for mechanism, T&S for policy, legal for removals
❌ Common L5 Trap
"I'll use a trie. Each node stores children and a frequency. On a query, I walk to the prefix node and do a DFS to find the top 10 by frequency."
Why this misses: The interviewer asks "how long does the DFS take for prefix 'a'?" — and the answer is "it visits most of the corpus." The design never set a budget, so it never noticed that query-time ranking can't meet it.
Drill 2: The Build Pipeline#
Prompt: "Walk me from raw query logs to what's in memory on a serving node."
Staff Answer
"Seven stages, run daily per locale (hourly for high-churn locales):
- Collect: search logs (query text, locale, user key, timestamp, clicked suggestion?) flow through Kafka to the lake.
- Normalize: NFKC, locale-aware case folding, whitespace collapse, strip trailing punctuation. The same library runs at serve time.
- Aggregate with decay: score = Σ over days of
count_d × 0.5^(age_d / 7), plus a CTR term from suggestion acceptance. Count distinct users per query. - Privacy filter: drop queries with < k distinct users over the window (e.g., k = 100 over 30 days). This removes the vast majority of distinct raw queries and all personal-looking ones.
- Policy filter: blocklists, classifiers (sexual, hate, violence, dangerous, personal information), entity-specific rules. Rejected queries are logged with reason.
- Build top-K: for every prefix of length 1..20 of every surviving query, keep top 20 by score. Emit as an FST or sorted key-value file with a checksum.
- Validate and publish: golden-set test (10K prefixes with expected results), size and empty-rate checks, diff vs previous snapshot. Upload to object storage; nodes pull and swap atomically in a staged rollout.
The serving node loads the snapshot into memory (memory-mapped), builds no structures at load, and swaps a pointer."
Why this is L6:
- Privacy and policy filters are pipeline stages, not afterthoughts
- Normalization shared between build and serve to prevent mismatches
- Validation and staged rollout treat the index as a deployable artifact
What L7 adds:
- Makes the build a shared platform: surfaces plug in their own scoring function but inherit privacy and policy stages they can't skip
- Records the policy version in the snapshot metadata so every suggestion shown can be traced to the policy that admitted it
❌ Common L5 Trap
"Every time someone searches, increment the count in the trie and update top-K up the path."
Why this misses: It turns the hottest read structure into a write-heavy one, and it publishes any query the moment it's searched enough — no privacy threshold, no policy check.
Drill 3: The Latency Budget — Make It Concrete#
Prompt: "Where does the 100ms go, and what do you cache where?"
Staff Answer
"Budget per keystroke:
| Segment | Budget |
|---|---|
| Debounce | 50–100ms (not counted if the user is still typing) |
| Client cache check | < 1ms |
| Network RTT to edge | 10–40ms |
| Edge hit | ~1ms |
| Edge → origin (on miss) | 20–40ms |
| Server lookup + override + serialize | 2–10ms |
| Render | ~10ms |
Caching layers:
- Client: cache every response by (locale, prefix) for the session. Backspace and retyping hit the cache. Also: if the response for 'how to t' has fewer than K results, the result for 'how to ti' is a filter of it — no request needed. Typically 15–25% of keystrokes.
- Edge: non-personalized responses, TTL 5–15 min, cache key = (locale, normalized prefix, client type). Short prefixes are few and hot, so hit rates for length ≤ 3 are >95%; overall edge hit rate 40–60% of requests reaching it.
- Server: the whole index is in RAM; no separate cache.
Net: origin sees ~30–50% of keystroke requests."
Why this is L6:
- Itemizes the budget and shows the server is a small slice
- Uses the prefix-subsumption trick on the client
- Separates personalized from cacheable responses in the cache key
What L7 adds:
- Prices it: edge requests cost ~10× less than origin requests at this scale; raising edge TTL from 5 to 15 min for prefixes ≤ 3 cuts origin load measurably with no freshness loss users notice
- Sets an org-wide client caching policy so the mobile app doesn't cache suggestions for 24h (which breaks removals)
❌ Common L5 Trap
"Put Redis in front of the service to cache popular prefixes."
Why this misses: The index is already in RAM; Redis adds a network hop and saves nothing. The caches that matter are the ones closer to the user.
Drill 4: Trending Without Brigading#
Prompt: "A celebrity announces a new album at 9pm. Users expect it in suggestions by 9:15. How?"
Staff Answer
"A streaming job reads the query log (Kafka) and maintains per-query counts in 5-minute tumbling windows, plus a 7-day baseline per query from the base index. A query is a trending candidate when:
- rate in the last window ≥ 5× its baseline (or it's new with ≥ N searches),
- ≥ 500 distinct users and ≥ 100 distinct /24 networks in the window,
- median account age and reputation above thresholds,
- it passes the same policy classifiers as the base build.
Candidates are added to a per-locale overlay (cap ~100K entries) and pushed to serving nodes every 1–5 minutes. At serve time, the node merges overlay candidates for the prefix with base top-K using a calibrated score, applies overrides, returns top 10. Overlay entries expire after 24h unless the next base build picks them up.
So the album shows up within ~10 minutes. A brigade from a few hundred accounts on a handful of networks doesn't."
Why this is L6:
- Uses diversity gates, not raw counts
- Keeps the trending path small and separately expiring
- Applies the same policy classifiers to the fast path
What L7 adds:
- Lets T&S tune gate thresholds per locale and vertical without a deploy, with an audit log
- Decides with T&S that trending entries mentioning named individuals require human review in sensitive verticals — accepting slower freshness there on purpose
❌ Common L5 Trap
"Lower the rebuild interval to every minute."
Why this misses: Rebuilding a 50 GB index every minute is expensive, and it publishes everything — including brigades — at the same speed as real news.
Drill 5: Hot Prefix and Hot Shard#
Prompt: "You sharded by first letter. The 's' shard is at 90% CPU and 'x' is idle. Fix it."
Staff Answer
"First question: why shard at all? A filtered prefix table for a large locale is ~20–50 GB. That fits on a 64–128 GB node. I'd replicate the full table to every node in the locale's pool and load-balance round-robin — no routing, no hot shards, add replicas linearly for QPS.
If the table genuinely doesn't fit — say a multi-locale pool at 300 GB — I'd shard by hash of the full normalized prefix (even key spread) and replicate prefixes of length ≤ 4 to every shard (a few hundred MB), because those are the hottest keys. Routing is by hash; any node can answer a short prefix locally.
And upstream, the edge absorbs >95% of length ≤ 3 prefixes anyway, so origin hotness is dampened."
Why this is L6:
- Questions the sharding premise with arithmetic
- Uses hybrid replication for the specific skew of prefix traffic
- Remembers the edge already flattens the hottest keys
What L7 adds:
- Compares cost: replication multiplies RAM (N replicas × 50 GB) but removes a routing tier and an on-call class; at typical cloud prices the RAM is cheaper than the operational complexity until tables reach hundreds of GB
❌ Common L5 Trap
"Split 's' into 'sa–sm' and 'sn–sz'."
Why this misses: It's manual rebalancing that has to be redone whenever traffic shifts, and it never questions whether sharding was needed.
Drill 6: Personalization Without Losing the Cache#
Prompt: "Product wants suggestions personalized by the user's history. How do you add that without blowing up cost?"
Staff Answer
"Three levels, cheapest first:
- Client-side recent queries: the app stores the user's last ~100 searches locally. On each keystroke, it merges matching recent queries (marked with a history icon) above the global list from the edge. Zero server cost; global response stays public-cacheable.
- Contextual but cacheable: coarse context like country, city or device type goes into the cache key. Cardinality stays bounded (hundreds of values), so edge caching still works.
- Server-side personal re-rank: for signed-in users where measurement shows gains, the server fetches a per-user candidate set (~1ms KV lookup — recent queries, followed entities) and re-ranks global top-20. Response is
Cache-Control: private. I'd expect this to roughly double origin requests for affected users, so it should be justified by a measured CTR or keystrokes-saved gain.
Privacy: history is deletable, retention-limited, and personalized suggestions never appear on signed-out or shared-device sessions."
Why this is L6:
- Orders options by cost and keeps the common path cacheable
- Uses bounded-cardinality context in cache keys
- States the privacy obligations of personalization
What L7 adds:
- Requires a privacy review because personalization changes what the company stores about users
- Makes "personalization lift per dollar" a decision metric reviewed with product
❌ Common L5 Trap
"Build a per-user trie of their search history and query it with the global trie."
Why this misses: Per-user tries for hundreds of millions of users is a large storage problem for a merge that a client can do locally with a 100-item list.
Drill 7: Build vs Buy#
Prompt: "Why not just use Elasticsearch's completion suggester?"
Staff Answer
"For many products, I would. Below ~10K QPS, with a corpus under tens of millions of entries and no heavy safety requirements — an e-commerce catalog, a docs site — the completion suggester is a fine choice: in-memory FST, weights, context filters, and the team probably already runs Elasticsearch.
At 1M QPS for web-scale query completion I'd build a dedicated lookup service, because:
- the serving logic is trivial (hash lookup + filter) and a dedicated service hits 10ms p99 on far fewer nodes than a general search cluster;
- the build pipeline — privacy thresholds, policy, decay, trending — is the real system, and it lives outside Elasticsearch either way;
- immutable snapshot swaps and staged rollouts are simpler than reindexing a live cluster.
So: buy the engine at small scale; at large scale the 'engine' is ~1,000 lines and the pipeline is the product."
Why this is L6:
- Gives a threshold for each answer, not a blanket preference
- Identifies where the real complexity lives (pipeline, not lookup)
What L7 adds:
- Considers who will run it in 3 years: a bespoke service needs an owning team; if search infra already runs Elasticsearch fleet-wide, the marginal cost of one more use case may win even at moderate scale
❌ Common L5 Trap
"Elasticsearch handles search, so it handles autocomplete."
Why this misses: It treats autocomplete as a query feature, ignoring the latency budget, the QPS multiple over search and the build-time safety pipeline.
Drill 8: Changing the Blocklist or Ranking Without an Incident#
Prompt: "T&S wants a new classifier that removes 'dangerous' suggestions. How do you ship it?"
Staff Answer
"A new classifier changes what millions of users see, so it ships like a ranking change:
- Shadow: run it in the build, but only log what it would remove. Report: % of prefixes affected, top-1,000 removed suggestions by traffic, per locale.
- Review: T&S samples removals for false positives — 'how to kill a python process' shouldn't go. Tune thresholds or add allowlists.
- Canary: ship a snapshot with the classifier to 1% of traffic. Compare suggestion CTR, keystrokes saved, empty-rate and complaint reports vs control.
- Roll out by locale, starting with locales with the best classifier quality.
Emergency removals don't go through this path — they use overrides, which take effect in minutes."
Why this is L6:
- Measures false positives before enforcement
- Uses snapshot canaries with quality metrics
- Separates policy evolution (slow, measured) from emergency removal (fast)
What L7 adds:
- Requires per-locale classifier quality bars before enabling — a classifier that's good in English may be poor in Thai
- Publishes policy changes in the external suggestion policy so users and regulators see a consistent standard
❌ Common L5 Trap
"Add the classifier to the pipeline and run the next build."
Why this misses: A 2% false-positive rate across billions of suggestions removes legitimate completions at scale with no measurement until users complain.
Drill 9: Cost — Halve the Serving Bill#
Prompt: "Typeahead costs more than search itself. Cut it in half."
Staff Answer
"Typeahead often costs more because it gets ~5–8× the requests. Levers, by safety:
- Debounce 50 → 100ms on desktop: cuts requests ~20–30% for fast typists, imperceptible.
- Client prefix subsumption: if a response had fewer than K results, filter it locally for longer prefixes — removes 10–15% of requests.
- Edge TTL up for short prefixes (5 → 30 min for length ≤ 3): those change slowly; trending is merged at origin for longer prefixes.
- Prune the index: drop prefixes with no traffic in 30 days; often 30–60% smaller, so fewer, smaller nodes.
- Move server-side personalization back to the client where lift is marginal.
Together these typically cut origin requests 40–60%. I would not reduce replicas below N+2 per region or relax the override SLA."
Why this is L6:
- Attacks request volume, not just node cost
- Each lever has an expected effect and a risk
- Protects reliability and safety from cost cuts
What L7 adds:
- Reframes the metric as cost per 1,000 searches assisted, tied to the revenue value of searches — suggestions that save keystrokes drive more completed searches, so the right budget is relative, not absolute
❌ Common L5 Trap
"Use smaller instances."
Why this misses: The index must fit in RAM; smaller instances force sharding and routing — more complexity, likely more cost.
Drill 10: Multi-Language and Multi-Region#
Prompt: "We're launching in Japan, China and the Middle East. What changes?"
Staff Answer
"Three things change: input, matching and policy.
- Input (IME): in Japanese and Chinese, the text in the box during composition is often phonetic (romaji/kana, pinyin) before conversion. The client must decide whether to send composition-state text; I'd send it and index readings alongside surface forms, so 'とうき' and 'touki' can match '東京' completions. Pinyin initials ('bj' → '北京') are common and need their own prefix keys.
- Matching: no whitespace word boundaries in CJK — prefix matching on the full string works for query completion, but mid-string matching needs a segmenter. Arabic and Hebrew are right-to-left with optional diacritics; normalize by stripping diacritics and unifying letter variants (e.g., alef forms). Turkish needs locale-aware casing.
- Policy: classifiers and blocklists are per locale and need native-speaker review; legal removal requirements differ by country.
Per-locale snapshots served from in-region pools (Tokyo, Hong Kong/Singapore, Middle East region) keep RTT low; the build pipeline runs centrally unless data-residency rules require in-country processing of query logs."
Why this is L6:
- Knows IME composition and readings are the core CJK problem
- Handles normalization differences per script
- Treats safety as per-locale, not translated
What L7 adds:
- Checks data-residency obligations before choosing a central build — in some jurisdictions query logs must be processed in-country, which turns one pipeline into several
- Staffs locale expertise: native-speaker T&S reviewers are a headcount decision that gates launch
❌ Common L5 Trap
"Use UTF-8 and the same trie."
Why this misses: Encoding is not the problem; matching phonetic input to characters and per-locale policy are.
8. Deep Dive Scenarios#
Deep Dive 1: Peak-Traffic Incident — Breaking News Spike#
Context: A major breaking news event. Search traffic triples in 5 minutes; suggest traffic quadruples because users type slowly and retype. Origin p99 goes from 12ms to 300ms in two regions, and suggestions for the event's keywords are stale (the trending overlay is 20 minutes behind). The on-call escalates to you.
Questions to Surface First:
- Is the latency at the suggest nodes, the gateway, or the edge-to-origin path?
- Why is the overlay 20 minutes behind — is the streaming job lagging or blocked by gates?
- What's the edge hit rate now vs normal? Did a deploy or purge reset caches?
- Are we near node memory or CPU limits, or connection limits?
Typical L5 Approach: Scales out suggest nodes and waits. Checks the stream job's lag and restarts it. Correct moves, but new nodes take minutes to load a 40 GB snapshot, and restarting the stream job adds more lag.
Staff Approach: Finds that edge hit rate dropped from 55% to 20%: event keywords are new prefixes not in cache, and the edge TTL is short for long prefixes. Enables edge request coalescing (if not on), raises edge TTL for non-personalized responses to 5 minutes during the event, and turns off server-side personalization (load-shedding mode) to make more responses cacheable. Overlay lag: the stream job is fine; the gate requires 500 distinct users over a full 5-minute window and the window just closed — expected. Origin p99 recovers in 3 minutes without new nodes.
Principal Approach: Writes a "surge mode" into the design: a single switch that raises edge TTLs, disables server-side personalization, and increases debounce via remote config — pre-approved by product as a degraded mode. Sets a capacity target in terms of origin QPS at 0% edge hit rate for the top 1,000 prefixes, and adds a quarterly load test simulating a news spike. Makes the trending-latency SLO explicit and agreed with T&S, so "20 minutes behind" isn't an incident when it's the agreed safety budget.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Immediate (0–5 min) | Check edge hit rate and origin QPS by region; enable coalescing; extend TTLs via config |
| Triage | Per-stage latency on nodes; gateway pool usage; overlay lag vs gate window |
| Quick fix | Surge mode: disable server personalization, raise debounce 75 → 150ms via remote config |
| Guardrails | Keep override propagation working — surge mode must not slow removals |
| Post-mortem | Pre-warm nodes; snapshot load time; capacity at low edge hit rate |
Metrics to Watch:
edge.hit_rate, edge.origin_requests, suggest.p99{stage}, gateway.pool_utilization, trending.overlay_age_seconds, node.snapshot_load_seconds.
Organizational Follow-up: Product signs off on the surge-mode degradations ahead of time. Snapshot loading time becomes a tracked metric (target < 60s via memory-mapped files).
Ownership Question: "Who decides to switch on surge mode?" Staff answer: The search-infra on-call, without escalation, because the degraded behavior is pre-approved by product. That's the point of pre-approving it.
Key Takeaway: "In a surge, the edge is your capacity. Design the degraded mode that makes more responses cacheable, and get it approved before the surge."
What clears the Staff bar:
- Finds the bottleneck at the cache layer rather than scaling the origin blindly
- Distinguishes expected trending latency from a fault
- Uses pre-approved degraded modes
Deep Dive 2: Silent Failure — The Index That Stopped Updating#
Context: A product manager notices that suggestions for a product launched 9 days ago never appear. You find the base index for three locales hasn't been rebuilt in 11 days. No alert fired. Serving is healthy.
Questions to Surface First:
- Did the build fail, or succeed but fail validation, or succeed and fail to publish?
- Why didn't anything alert on snapshot age?
- Are other locales also stale but unnoticed?
- Is the trending overlay masking staleness for popular queries?
Typical L5 Approach: Finds the failed job (a schema change in the log table broke the normalization step for those locales), fixes it, reruns the build, adds an alert on job failure.
Staff Approach: Fixes and rebuilds, but alerts on the outcome, not the job:
index.age_hoursper locale on serving nodes, paging at 36h. Also finds that validation correctly rejected two builds — but rejection went to a log, not a person. Adds a contract test for the log schema the pipeline depends on, owned jointly with the logging team.
Principal Approach: Generalizes: any system serving precomputed artifacts (suggestions, recommendations, spell-correction dictionaries, ranking models) can go silently stale. Introduces a standard: every served artifact exposes its age and version as a metric, with an owner and a max-age SLO. Adds upstream schema changes to a change-review process with downstream consumers registered.
Staff Approach — Full Reasoning
| Dimension | Staff Answer |
|---|---|
| Root cause | Upstream schema change broke normalization; validation rejected builds; rejection unalerted |
| Immediate action | Fix parser, rebuild the three locales, staged rollout |
| System fix | Alert on index.age_hours at serving, not job status |
| Process fix | Schema contract tests; registered downstream consumers for log tables |
| Broader question | Which other served artifacts have no age metric? |
Metrics to Watch:
index.age_hours{locale}, build.validation_rejections_total, build.success{locale}, trending.overlay_share (rising share of results from overlay can indicate stale base).
Organizational Follow-up: Logging team adds search-suggest as a registered consumer; schema changes require their sign-off.
Ownership Question: "Who owned noticing the stale index?" Staff answer: Search infra — the serving team owns the freshness of what it serves, regardless of which pipeline produced it. Job-level alerts were a proxy; age at serving is the real signal.
Key Takeaway: "Alert on the age of what you serve, not on the success of the job that builds it."
What clears the Staff bar:
- Moves alerting from process to outcome
- Notices validation rejections need a human owner
- Fixes the upstream contract, not just the parser
Deep Dive 3: Large Onboarding — A New Surface Adopts the Platform#
Context: The shopping team wants to retire its own autocomplete and use the shared suggestion platform. They have 80K QPS peak, a catalog of 200M products, need suggestions to reflect price drops and inventory within 15 minutes, and have their own blocklist of 30K terms. Launch in 6 weeks.
Questions to Surface First:
- Are they completing queries or products (entities)? Probably both.
- Does inventory need to be in the suggestion index, or can it be a serve-time filter?
- Is their blocklist stricter than the platform floor, looser, or overlapping?
- Who owns relevance regressions after migration — them or the platform?
Typical L5 Approach: Adds a "shopping" locale-like index to the build, imports their blocklist, and capacity-plans for 80K QPS.
Staff Approach: Splits their needs: query completion uses the shared build with a shopping-specific scoring function plug-in; product entity completion is a separate candidate source (top products per prefix) merged at serve time; inventory and price are serve-time filters from a small fast store, so the index doesn't rebuild every 15 minutes. Their blocklist is layered on top of the platform floor as surface-specific rules. Migration runs in shadow for 2 weeks comparing CTR against their current system.
Principal Approach: Uses the onboarding to define the platform's surface contract: what a surface can customize (scoring plug-in, candidate sources, stricter rules, UI) and what it cannot (privacy threshold, policy floor, override service, propagation SLA). Agrees on an SLO split — platform owns availability and latency; surface owns relevance — and a chargeback model so capacity growth is funded by the surface that drives it.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Week 1–2 | Define scoring plug-in and entity candidate source; serve-time inventory filter design |
| Week 2–3 | Import blocklist as surface rules; T&S reviews overlap with platform floor |
| Week 3–4 | Capacity: 80K QPS on dedicated pool; load test with their traffic replay |
| Week 4–6 | Shadow: serve both, log both, compare CTR and keystrokes saved; cut over by percentage |
Metrics to Watch:
suggest.ctr{surface}, suggest.keystrokes_saved{surface}, suggest.p99{surface}, inventory_filter.drop_rate.
Organizational Follow-up: Platform runbook includes surface-specific escalation contacts; shopping on-call handles relevance pages.
Ownership Question: "If shopping suggestions get worse after migration, who's paged?" Staff answer: Shopping's on-call for relevance metrics; platform on-call for latency and availability. That split is written into the onboarding contract before launch.
Key Takeaway: "Onboarding is where a platform defines its contract. Write down what surfaces can customize and what they can't before the first one migrates."
What clears the Staff bar:
- Moves fast-changing data (inventory) to serve-time filters
- Layers surface rules on a non-negotiable floor
- Splits SLO ownership explicitly
Deep Dive 4: Post-Mortem — The Suggestion That Made the News#
Context: For six hours, typing a politician's name suggested a false and defamatory completion. It reached social media and press. The suggestion passed the privacy threshold (thousands of distinct users searched it after a viral post) and no classifier flagged it. You're leading the post-mortem.
Questions to Surface First:
- Did it enter via the base build or the trending overlay?
- How long from first report to removal at each layer — nodes, edge, clients?
- Were there earlier signals (user reports, T&S queue) that were missed?
- Are other public figures exposed to the same pattern right now?
Typical L5 Approach: Adds the phrase to the blocklist, rebuilds, and adds the politician's name to a sensitive-entity list.
Staff Approach: Timeline shows it entered via trending (legitimate diversity — a real viral wave), removal took 6 hours because the override console required an engineer, and mobile clients cached it for 24h. Fixes: T&S operators can create overrides directly (audited); client-side deny-set; edge push-replace. Pattern fix: completions pairing a person entity with an allegation-type term ("is a", "arrested", "affair") require classifier scoring and, above a traffic threshold, human review before entering trending.
Principal Approach: Treats it as a policy and governance gap, not a classifier gap. Establishes an org-level suggestion policy for named individuals with legal and policy teams, a defined removal SLA (e.g., 30 minutes from report for high-severity), a 24/7 T&S staffing decision for that SLA, and quarterly removal-propagation drills across web, mobile and edge. Reports time-to-removal to leadership as a risk metric alongside availability.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Immediate | Override at nodes; targeted edge push-replace; client deny-set push via config |
| Scope | Scan current trending and base for person + allegation patterns; review top hits |
| Remediation | Direct T&S override access; audit log; client cache TTL ≤ 1h |
| Guardrails | Person-entity + allegation classifier; human review threshold on trending |
| Post-mortem | Removal latency per layer; why reports didn't escalate; on-call access path |
Metrics to Watch:
override.propagation_seconds{layer}, tns.report_to_removal_minutes, trending.person_allegation_candidates, synthetic probe failures.
Organizational Follow-up: Removal drill each quarter: create a test override and measure time to absence at nodes, edge and each client version.
Ownership Question: "Who decides what's removed?" Staff answer: Trust & Safety under a written policy, with legal for court orders. Search infra owns the mechanism and its SLA — engineers should never be the ones judging content at 2am.
Key Takeaway: "The removal path is the safety system. Measure it end to end, including caches you don't own."
What clears the Staff bar:
- Measures removal latency per layer, including clients
- Separates policy decision (T&S) from mechanism (infra)
- Fixes the pattern class, not the single string
Deep Dive 5: Multi-Region Expansion#
Context: Typeahead runs from two US regions. Leadership wants sub-50ms p95 keystroke latency in Europe, India and Southeast Asia, and EU data protection review flagged that EU users' query logs are processed in the US.
Questions to Surface First:
- What's current p95 for those users, and how much is RTT vs server?
- Does the EU requirement apply to query logs (personal data) or also to derived aggregates?
- Do overrides need to be global, regional, or both (some removals are country-specific)?
- What's the edge hit rate in those regions?
Typical L5 Approach: Deploys suggest nodes in each new region, replicates the snapshot store, and routes users by geo-DNS.
Staff Approach: Serving is easy to regionalize because snapshots are immutable files — replicate them to regional object storage, nodes pull locally. The harder part is the build: EU query logs are processed in an EU pipeline that produces EU-locale snapshots; aggregated, k-anonymized prefix tables can then be served anywhere. Overrides are a global replicated set with per-country scoping (a court order in one country applies only there). Edge caching in those regions handles most traffic.
Principal Approach: Separates two decisions with different owners and reversal costs: serving locality (an engineering decision, two-way door) and data processing location (a legal one-way door). Gets legal to confirm in writing that k-anonymized aggregates are outside personal-data scope before designing around it. Prices the regional build pipelines (each adds compute plus an on-call surface) and consolidates where regulation permits.
Staff Approach — Full Reasoning
| Phase | What to Do |
|---|---|
| Measure | p95 split into RTT / edge / origin per region |
| Serve | Regional pools; snapshot replication; geo routing with failover to nearest region |
| Build | EU pipeline for EU logs; aggregates exported after k-anonymity |
| Overrides | Global set with country scope; replicated with < 1 min lag |
| Rollout | One region at a time; compare latency and CTR before/after |
Metrics to Watch:
suggest.e2e_p95{country}, snapshot.replication_lag{region}, override.replication_lag{region}, edge.hit_rate{region}.
Organizational Follow-up: Data-protection review of the EU pipeline; regional on-call coverage plan.
Ownership Question: "Who decides where query logs may be processed?" Staff answer: Legal and the privacy office decide; search infra implements and documents. It's a one-way door, so it gets a written decision, not a Slack thread.
Key Takeaway: "Immutable snapshots make serving trivially regional. The hard part is where the raw data may be processed."
What clears the Staff bar:
- Separates serving locality from data-processing locality
- Scopes overrides by country
- Measures RTT before adding regions
9. Level Expectations Summary#
After studying this case study, you should be able to:
- Distinguish query completion, entity typeahead and command completion, and pick one with reasons
- Derive a per-keystroke latency budget and show where each millisecond goes
- Explain why top-K per prefix is precomputed offline, and size the resulting index
- Place caches at the client, the edge and the server, and explain how personalization affects cacheability
- Design a trending overlay with diversity gates that resists brigading
- Design the safety pipeline: k-anonymity, classifiers, overrides with an end-to-end propagation SLA
- Choose replication over sharding when the index fits, and handle hot prefixes when it doesn't
- Handle IME input, script-specific normalization and per-locale policy
The Bar for This Question#
Mid-level (L4): Builds a trie with frequencies, finds completions by traversal, maybe caches popular prefixes. Works for a demo; doesn't address latency at scale, freshness or safety.
Senior (L5): Precomputes top-K per node, serves from memory, shards and replicates, adds a cache, rebuilds periodically. Solid and correct. Tends to treat freshness as "rebuild faster", safety as a word list, and caching as one Redis tier; doesn't price personalization.
Staff+ (L6): Starts from intent and a per-keystroke budget. Moves all ranking offline into a pipeline with privacy and policy stages, publishes immutable versioned snapshots with staged rollout, layers caching from client to edge, adds a gated trending overlay, and owns an override path with a measured propagation SLA across nodes, edge and clients. Names owners: search infra for mechanism, T&S for policy, legal for removals. Handles non-Latin input. The interviewer should learn something from the answer.
10. Staff Insiders: Controversial Opinions#
10.1 "The Trie Is Not the Interview"#
| Evidence | Detail |
|---|---|
| Serving logic | Hash lookup + filter; a few hundred lines |
| Where incidents come from | Bad snapshots, removals, brigades, cache stampedes, i18n bugs |
| Where cost comes from | Request volume, not index structure |
The Staff position: Spend 2 minutes on the data structure — "precomputed top-K per prefix, FST or hash table, immutable snapshot" — and 40 on budget, caching, freshness and safety.
Why this matters in interviews: Interviewers who've asked this question many times have seen every trie. Moving past it quickly signals you know where the work is.
10.2 "Real-Time Suggestions Are a Liability, Not a Feature"#
| Evidence | Detail |
|---|---|
| Abuse path | Fastest path from searches to suggestions is the brigade path |
| User expectation | Minutes-fresh trending meets breaking-news needs |
| Review | Seconds-fresh leaves no time for any check |
The Staff position: Minutes, gated, with an overlay that expires. Seconds-fresh suggestions for open-ended queries create more incidents than value.
Why this matters in interviews: Pushing back on "real-time" with a reasoned safety argument is a classic Staff-level signal.
10.3 "Replicate the Whole Index — Sharding Is Usually Premature"#
| Evidence | Detail |
|---|---|
| Index size | 20–50 GB per large locale after filtering and pruning |
| Node RAM | 64–256 GB is routine |
| Sharding cost | Routing tier, hot shards, rebalancing, partial-failure modes |
The Staff position: Replicate until the table doesn't fit, then shard only the long tail.
Why this matters in interviews: "Shard by first letter" is the most common L5 reflex; arithmetic that shows it's unnecessary is memorable.
10.4 "The Best Personalization Happens on the Client"#
| Evidence | Detail |
|---|---|
| User history | Already on the device |
| Cacheability | Server personalization makes responses private |
| Privacy | Less server-side storage of personal history |
The Staff position: Merge recent history on the client; reserve server personalization for cases with measured lift.
Why this matters in interviews: It shows you see caching, cost and privacy as one decision.
10.5 "Suggestion Safety Is a Staffing Decision"#
| Evidence | Detail |
|---|---|
| Removal SLA | Only as fast as the human who decides |
| Classifiers | Need per-locale native review |
| Legal removals | Require a policy owner, not an engineer |
The Staff position: The override mechanism is easy; a 30-minute removal SLA needs 24/7 T&S coverage and per-locale reviewers. Say so.
Why this matters in interviews: Naming the people cost of a safety requirement is a bridge into the L7 conversation.
11. The Principal Lens (L7)#
Why L7 Sees This Problem Differently#
A Staff engineer designs an autocomplete. A Principal engineer notices the company has six of them — web search, maps, shopping, help center, the mobile app's search, and an internal tool someone open-sourced — each with its own blocklist, its own privacy threshold (or none), and its own idea of how fast a removal happens. The next suggestion that makes the news will come from whichever is weakest. At org scale, typeahead is a publishing platform: a shared build pipeline with mandatory privacy and policy stages, one override service with an audited removal SLA, and surface-owned ranking on top. The lookup service is the least interesting part.
🧭 Principal Move: "Before designing another suggester, I'd like to know how many exist and how each handles a removal request. If a court order today means six tickets to six teams, the design we need is one override service and one policy floor — and this surface is its first customer."
The Org-Level Fault Line#
One suggestion platform vs per-surface suggesters.
| Option | What Works | What Breaks | Who Pays |
|---|---|---|---|
| Per-surface suggesters | Each team tunes for its domain; fast iteration | N blocklists, N privacy thresholds, N removal paths; weakest one sets the risk | T&S and legal (N integrations), PR (incidents) |
| One central suggester for everything | Consistent safety | Central team bottleneck for every surface's ranking changes | Product velocity |
| Shared platform: mandatory safety + build, surface-owned ranking (L7 default) | One removal path and policy floor; surfaces own scoring, candidates, UI | 2–4 engineer-quarters to build; requires a surface contract | Platform budget up front; pays back at the second surface |
The deciding question: how many surfaces show suggestions to the public? One: build it well. Two or more: the safety floor must be shared; ranking can stay local.
Cost Model#
Assumptions: cloud list prices, memory-optimized nodes ~$1–2K/month (128–256 GB RAM), CDN requests ~$0.50–$1 per million, loaded engineer cost ~$25K/month.
| Scale | Architecture | Infra $/month | Headcount | On-Call Load |
|---|---|---|---|---|
| ~1K QPS, 1 locale | Search engine completion suggester or small service; daily build; blocklist | ~$1–3K | ~0.5 FTE | Shared with search; rare pages |
| ~100K QPS, 10 locales, 2 regions | Dedicated lookup service (replicated), edge caching, daily build + trending overlay, override service | ~$25–60K (edge ~40%) | 3–5 FTE + T&S reviewers | Own rotation; ~2–4 pages/month |
| ~1M QPS, 40+ locales, 5+ regions, multiple surfaces | Suggestion platform: shared build, regional pools, client deny-sets, per-surface plug-ins | ~$200–500K | 10–15 FTE platform + per-surface + 24/7 T&S | Per-region coverage; quarterly removal drills |
The line that matters to leadership: at 1M QPS peak, every 10 points of edge hit rate is roughly the cost of a regional serving pool. And the dominant non-infra cost is people: a 30-minute, 24/7 removal SLA across 40 locales is a T&S staffing commitment larger than the serving team.
The 3-Year Evolution Path#
Each step is triggered by an event. Building a multi-surface platform before a second surface exists is how a team spends a year on abstractions nobody uses.
One-Way Doors vs Two-Way Doors#
| Decision | Door | Reversal Cost | Why |
|---|---|---|---|
| Data structure (FST vs hash table) | Two-way | Days | Hidden behind the snapshot format |
| Replicate vs shard | Two-way | Weeks | Routing layer can be added later |
| Edge TTLs and debounce | Two-way | Minutes (config) | Should be remote-configurable |
| Client caching behavior in shipped app versions | One-way-ish | Months (app update adoption) | Old app versions cache for as long as they were told to |
| k-anonymity threshold | One-way in the loosening direction | Can't un-publish what leaked | Lowering it exposes private queries permanently |
| Where query logs are processed | One-way | Legal and regulatory | Data-residency commitments |
| Public suggestion policy | One-way-ish | Regulatory and press scrutiny | Once published, changes are visible and judged |
🧭 Principal Insight: Spend review time proportional to reversal cost. The FST debate gets an hour. Client cache TTLs in the mobile app, the privacy threshold and the log-processing location get a review with privacy, legal and mobile in the room — because those are the ones you can't take back quickly.
The Standard I'd Write#
RFC: Public Suggestion Surfaces Standard (v1)
Scope: Any product surface that shows auto-generated suggestions, completions or related searches to users.
Requirements:
- Suggestions derived from user behavior MUST pass a k-anonymity threshold of ≥ k distinct users over ≥ d days (values set by the privacy office per surface).
- Every suggester MUST apply the shared override service on every response and MUST meet the removal propagation SLA (p99 ≤ 10 minutes to serving, edge and supported clients).
- Client caches of suggestions MUST NOT exceed 1 hour TTL and MUST apply the client deny-set.
- Index builds MUST record the policy version and pass golden-set validation before rollout; rollouts MUST be staged.
- Surfaces SHOULD use the shared build pipeline; surfaces that do not MUST document equivalent privacy and policy stages.
Exceptions: Filed with the search platform architecture group and T&S; decision within 5 business days; maximum 2 quarters.
Success metrics: All public suggesters on the override service within 3 quarters; removal p99 ≤ 10 min measured by quarterly drill; zero suggestions shown below the privacy threshold; number of independent blocklists reduced to one floor plus surface rules.
What I'd Tell the VP#
"Search suggestions are the highest-traffic thing our search product does, and every one of them is something we say to users before they've asked. Today six products generate suggestions with six different safety processes, and the last removal request took six hours. I'm proposing one shared suggestion platform with a common safety floor and a 10-minute removal guarantee, while each product keeps control of its own ranking. It costs about four engineers for a year plus round-the-clock trust-and-safety coverage we partly have already. The return is that the next problem suggestion is gone in minutes instead of in the news, and new products get autocomplete in weeks instead of building their own."
Principal Interview Signals#
| Signal | What It Sounds Like |
|---|---|
| Inventories before designing | "How many suggesters exist today, and how does each handle a court-ordered removal?" |
| Prices the keystroke | "Each 10 points of edge hit rate is a regional pool. I'd spend on the edge before the origin." |
| Separates policy from mechanism | "T&S decides what's removed; the platform guarantees it's gone in 10 minutes everywhere." |
| Names one-way doors | "Client cache TTLs ship in app binaries. That's the decision I want reviewed, not the trie." |
| Treats safety as staffing | "A 30-minute 24/7 removal SLA in 40 locales is a headcount plan, not a feature." |
Staff answers that L7 interviewers find insufficient:
- "We'll add an override layer for removals." — correct, but silent on the clients and other surfaces that don't use it.
- "Trending is gated on distinct users." — right mechanism, but no owner for the thresholds and no per-locale review capacity.
- "We'll replicate the index to every region." — sound serving design that never asks where the query logs may legally be processed.
Appendices
Appendix A: Data Structures and the Build Pipeline
A.1 The Build Pipeline#
A.2 Top-K Construction (pseudocode)#
for query q in suggestible_queries(locale):
s = score(q) # decayed count + CTR term
for i in 1..min(len(q), L):
p = q[0:i]
heap[p].push((s, q)) keep top N # bounded heap per prefix
for p in heap:
emit(p, sorted(heap[p])) # sorted key order for FST
In practice this is a distributed job: emit (prefix, score, query) pairs, group by prefix, keep top N per group. Output is sorted by prefix and written as an FST or a sorted, front-coded key-value file.
A.3 Structure Comparison#
| Structure | Memory (50M queries) | Lookup | Update Model | Notes |
|---|---|---|---|---|
| Pointer trie + top-K per node | ~100–300 GB | O(L) | Rebuild or locked mutation | Pointer overhead dominates |
| Hash map prefix → top-K | ~40–100 GB | O(1) | Rebuild | Simplest; easy to shard |
| FST with outputs | ~10–40 GB | O(L) | Rebuild only | Most compact; used by Lucene |
| Sorted array + binary search | ~30–60 GB | O(log n) | Rebuild | Memory-map friendly |
A.4 Long-Prefix Fallback#
For prefixes longer than L: look up the length-L prefix, filter its N candidates by the full prefix. If none match (rare, <2%), query a long-tail index (e.g., a search engine with edge n-grams over suggestible queries) with a 30–50ms budget; if it times out, return empty — an empty list is better than a late one.
Appendix B: Ranking Signals and Freshness
B.1 Signals#
| Signal | Where Computed | Typical Weight Role |
|---|---|---|
| Decayed query frequency | Build | Primary popularity |
| Suggestion acceptance rate (CTR) | Build | Corrects popularity for usefulness |
| Downstream success (search clicked a result) | Build | Penalizes queries that lead nowhere |
| Freshness / trending ratio | Stream overlay | Boosts spiking queries |
| Locale and country | Build (separate indexes) or cache key | Relevance |
| User's recent queries | Client or serve-time | Personalization |
| Policy demotion | Build + override | Safety |
B.2 Decay#
score(q) = Σ_d count(q, d) × 0.5^((today - d) / half_life)
half_life: 7 days (general), 1–2 days (news), 30 days (stable catalogs)
B.3 Serve-Time Merge#
Appendix C: Snapshot Lifecycle and Rollout
C.1 Snapshot States#
C.2 Rollout Gates#
| Stage | Duration | Gate |
|---|---|---|
| Single node | 10 min | No errors, memory within bounds |
| 5% | 1 hour | empty_response_rate within 10% of control; CTR within 2% |
| 50% | 1 hour | Same |
| 100% | — | Previous snapshot kept hot for instant rollback |
Nodes keep the previous snapshot memory-mapped so rollback is a pointer swap measured in milliseconds.
Appendix D: Caching, Client Behavior and the API Contract
D.1 Client Rules#
| Rule | Value | Why |
|---|---|---|
| Debounce | 50–100ms | Drops intermediate keystrokes for fast typists |
| Cancel in-flight on new keystroke | Always | Prevents out-of-order rendering |
| Ignore stale responses | Compare request sequence number | A slow response for 'ho' must not overwrite 'how' |
| Session cache | By (locale, prefix) | Backspace and retype are free |
| Prefix subsumption | If result count < K, filter locally | Avoids requests for longer prefixes |
| Cache TTL | ≤ 1 hour | Removals must reach clients |
| Deny-set | Refreshed with remote config | Filter cached responses after removals |
D.2 Edge Rules#
| Rule | Value |
|---|---|
| Cache key | locale + normalized prefix + client class |
| TTL | 5–15 min (≤ 3 chars: up to 30 min) |
| Personalized responses | Cache-Control: private, bypass edge |
| Request coalescing | On |
| Safety purge | Push-replace with filtered response, not delete |
D.3 Caching Decision Tree#
See CDN & Edge Caching and Caching for the general mechanics.
Appendix E: Safety, Privacy and Overrides
E.1 Defense Layers#
| Layer | Mechanism | Latency to Effect | Owner |
|---|---|---|---|
| Privacy threshold | ≥ k distinct users over d days | Next build | Privacy office sets k; infra enforces |
| Build-time policy | Blocklists, classifiers per locale | Next build | T&S |
| Trending gates | Diversity, reputation, classifiers | Minutes | T&S thresholds; infra mechanism |
| Serving override | Deny exact / pair / entity / pattern | < 2 min to nodes | T&S operates; infra owns SLA |
| Edge | Push-replace affected prefixes | < 5 min | Infra + CDN team |
| Client | Deny-set via remote config | < 10 min for active clients | Infra + mobile |
E.2 Override Matching#
| Match Type | Example | Use |
|---|---|---|
| Exact completion | "<string>" anywhere | Clear policy violations |
| Prefix + completion pair | prefix "<name> is" → completion "<term>" | Context-specific harms |
| Entity | Any completion about entity X | Legal removals about a person |
| Pattern | Regex/classifier class | Emergent abuse patterns |
E.3 Audit#
Every override records: who, when, match, reason code, ticket, scope (global / country), expiry. Overrides without expiry require T&S lead approval. Quarterly review removes stale overrides that no longer match anything.
Appendix F: Observability
F.1 Core Metrics#
# Latency and load
suggest.e2e_latency_p95{country} # client-measured, keystroke to render
suggest.p99{stage} # lookup, merge, override, serialize
edge.hit_rate{region}
edge.origin_requests
# Quality
suggest.ctr{locale,surface}
suggest.keystrokes_saved{locale}
suggest.empty_response_rate{locale}
# Freshness
index.age_hours{locale}
trending.overlay_age_seconds
trending.rejected_by_gate{reason}
# Safety
override.propagation_seconds{layer}
tns.report_to_removal_minutes
synthetic_probe.violations_total # must be zero
F.2 Critical Alerts#
| Alert | Condition | Severity |
|---|---|---|
| Latency | suggest.p99 > 25ms for 5 min | Page |
| Empty responses | > 2× baseline for a locale | Page |
| Stale index | index.age_hours > 36 | Page |
| Removal propagation | Synthetic probe finds removed suggestion after 10 min | Page (high severity) |
| Edge hit-rate drop | < 70% of baseline for 10 min | Warn |
Appendix G: Scale Evolution
G.1 What Works at Each Scale#
| Scale | Design | What Breaks Next |
|---|---|---|
| < 100K items, any QPS | Ship list to client | Corpus growth |
| < 10K QPS | Search engine completion suggester | Safety pipeline, freshness control |
| 10K–300K QPS | Dedicated replicated lookup, edge caching, daily build | Removals, trending |
| > 300K QPS, many locales | Platform with overlay, overrides, client deny-sets, regional pools | Multi-surface governance |
G.2 What You Don't Build on Day One#
- Real-time mutable index (batch + overlay is enough)
- Server-side personalization (start on the client)
- Sharding (replicate until it doesn't fit)
- Typo tolerance for short prefixes (only for ≥ 4 chars on miss)
- Per-surface ranking plug-in framework (until there's a second surface)