Skip to lesson
‹

Courses / System Design Interview / Chapter 33

Module 3 · Design Real Products · Chapter 33 of 39

Design Typeahead Suggestions

20 sections · ~40 min✓ Fact-checked

Builds on Back-of-the-Envelope Estimates, Content Delivery Networks, Distributed Caches and Distributed Search. Next up: Design Collaborative Document Editing.

Part 1 · Requirements and Scale

The brief

It is 10:40 pm, the last over of an India vs Australia final. Lakhs of people pick up their phones and type ind into a search app at the same moment. On Neha's phone in Kanpur the list under the box arrives only after her third letter, still offers last month's "india vs australia 2nd odi", then jumps back to suggestions for in while she is typing indi. Nothing is down. The suggestions are simply late, stale and out of order. This chapter designs a search box that answers every keystroke within about 100 ms, with tonight's queries in the list.

The product: the suggestion list that drops down under a search box. Show up to ten completions for the letters typed so far, favour what is popular right now, and never suggest anything offensive or private. About 50 million people search every day. Neha typing ind on final night is the example we follow throughout.

What the search box must do

Neha's problem was three problems: a slow list, a stale list and a list from the wrong keystroke. Start by pinning down what the feature must do. Type ind and a list should drop down before the next key press: "india vs australia live score", "indigo flight status", "indian railway pnr status". This is typeahead, also called autocomplete: given the prefix typed so far, return the few complete queries the user most likely wants.

It looks small, but it runs on almost every keystroke of every user, which gives it several times the traffic of search itself, and each answer has to arrive before the next key.

RequirementWhy it matters
FunctionalUp to 10 completions for the typed prefix, best firstThe whole point of the feature
FunctionalPopular and recent queries rank higherTonight's final should beat last month's match
FunctionalThe user's own recent searches can appear firstPeople repeat their searches a lot
FunctionalNo offensive, illegal or private suggestionsA suggestion reads like the product speaking
Non-functionalAbout 100 ms from keystroke to listA slower list lags behind the typing
Non-functionalHighly available, but allowed to degradeSearch must work even with no suggestions

The 100 ms target comes from typing speed. Someone typing 40 words a minute presses a key about every 300 ms; a fast typist, about every 150 ms. If suggestions take longer than the gap between keys, the list describes a prefix the user has already moved past. About 100 ms is also roughly where a response stops feeling instant.

Out of scope today

Spelling correction, the search results page and ads. They sit next to this feature but do not change its design.

How big it gets

Before drawing servers, count the requests, because every keystroke is one. Assume 50 million daily users who search 6 times a day. A query is about 20 characters, but the app skips keys during a fast burst and many people tap a suggestion early, so count about 8 suggestion requests per search.

Try it first: 50 M users × 6 searches × 8 requests a day. What is the average rate, and the evening peak at 3× the average? If the edge caches answer 60% of requests and one suggestion server handles about 10,000 a second, how many servers are busy at a planned peak of 100,000 a second?
  • 50 M × 6 × 8 = 2.4 B requests a day; ÷ 86,400 s ≈ 28,000 a second.
  • Evening peak ≈ 3 × 28,000 ≈ 83,000 a second; plan for 100,000.
  • 40% miss the edge: 40,000 a second ÷ 10,000 ≈ 4 busy servers.
QuantityWorkingResult
Suggestion requests300 M searches × 82.4 B a day, ≈ 28,000 /s
Planned peakevening ≈ 3×, rounded up100,000 /s at the edge
Big match burst5 to 10× for minutes, on a few shared prefixesmostly absorbed by the edge
Edge misses at peak100,000 × 40% (assumed)≈ 40,000 /s
Suggestion servers40,000 ÷ about 10,000 each (assumed)4 busy; run 9, 3 in each of 3 regions
Peak bandwidth100,000 /s × under 1 KBunder 100 MB/s, mostly from the edge
Search events logged300 M × about 100 B≈ 30 GB a day

Nine servers, not four, because a whole region can fail: the six left still have room for 40,000 a second. The big-match row is why the edge matters so much. A final multiplies traffic 5 to 10 times, but nearly everyone types the same few prefixes, which is exactly what a cache serves best.

How much memory the index needs

Requests decide how many servers answer; memory decides what each one can hold. We keep only queries that enough different people typed recently, about 100 million distinct queries across all languages, in a compressed trie that stores the top 10 answers at every node. The deep dives explain both ideas; first, estimate what they cost.

Try it first: 100 M queries in a compressed trie, with at most 2 nodes per query and about 60 bytes per node (10 query ids of 4 bytes, plus links), plus about 24 bytes of text and score per query. How much memory is one copy, and how much while a server swaps in a new one?
  • 200 M nodes × 60 B ≈ 12 GB.
  • 100 M × 24 B ≈ 2.4 GB of text and scores, so one copy is about 14.4 GB, call it 15 GB.
  • During a swap the old and new tries sit side by side: about 29 GB, comfortable on a 64 GB server.

Two checks before trusting that. First, 60 bytes a node assumes a compact layout: nodes packed into large arrays, with children and lists stored as 4-byte indexes. Built as one object per node with pointers, as a quick prototype would be, the same trie takes several times more and stops fitting. Second, is 100 M queries sane? Thirty days bring 300 M × 30 = 9 B searches, and a query is kept only if at least 50 different people typed it, so at most 9 B ÷ 50 = 180 M can qualify. Popular queries use up many searches each, so the real number is lower: 100 M is a generous ceiling.

This result shapes the whole design: a full copy of the index fits in one server's memory. Every suggestion server holds all of it, any server can answer any prefix, and we add servers for throughput, not for space.

Spending the 100 ms

Servers and memory are settled; now check that an answer can actually arrive in time. Split the 100 ms across every step a keystroke passes through, and give the biggest share to the part you control least: the mobile network.

StepBudgetHow to stay inside it
Key press to request sentabout 5 msa tiny GET; no heavy work on each key
Phone to the nearest edge and back30 to 60 msedge servers in many cities; reuse one open connection
Edge cache lookupunder 1 msshort prefixes are almost always cached
Edge to a region and back, on a miss10 to 40 mssuggestion servers in three regions; Mumbai to Delhi is about 30 ms
Suggestion server lookupunder 1 msthe trie is in memory; no disk, no database call
Drawing the listabout 10 msplain text rows, no images

Add it up honestly. An edge hit takes about 45 to 75 ms, comfortably inside the budget. A miss takes about 55 to 115 ms, so on a slow network it can run over. The server's own work is about 1 ms either way: almost all of the budget is network. That is why the design answers as many keystrokes as possible from the edge, and why a faster lookup would barely help.

Part 2 · API, Data and the Big Picture

The API

An answer can only come from the edge if the request is cacheable, so the API is designed around that. One read endpoint carries almost all the traffic. Make it a plain GET with everything in the URL, so the browser, the CDN and any proxy on the way can cache it.

http
GET /v1/suggest?q=ind&lang=en-IN&limit=10

200 OK
Cache-Control: public, max-age=300
{
  "prefix": "ind",
  "suggestions": [
    { "text": "india vs australia live score" },
    { "text": "indigo flight status" },
    { "text": "indian railway pnr status" }
  ]
}

POST /v1/events
{ "type": "search", "q": "indigo flight status", "picked_rank": 2, "lang": "en-IN" }

The suggest call never needs to know who is typing, which is what keeps it cacheable.

  • Normalise the prefix before using it as a cache key: lowercase it and collapse repeated spaces, so IND and ind share one entry. Keep a single trailing space, because ind (a finished word) means something different from ind.
  • lang picks the right index. People searching in Hindi, Tamil or English type different things.
  • limit is capped at 10. Nobody reads a longer list while typing.
  • max-age=300 lets the browser and the edge reuse an answer for 5 minutes. A longer time means more cache hits but staler lists.
  • POST /v1/events is sent when a search is submitted, with the rank of the suggestion the user picked, if any. It feeds the offline build, never the live path.

Personal suggestions, such as your own recent searches, deliberately do not come from this endpoint. The app keeps them on the device and merges them in, so the shared response stays identical for everyone.

The data model

The suggest call reads nothing but memory, so where does that memory come from? There are two worlds: raw logs that grow every second, and serving data rebuilt from them on a schedule. Keep them apart: the live path never reads a log.

search_eventstsq_normalisedlanguser_hashpicked_rank
query_dailyq_normalised (pk)lang (pk)day (pk)countusers_sketch
trie_snapshotsversion (pk)lang (pk)file_keybuilt_atquery_count
blocklistpattern (pk)reasonadded_byadded_at
DataSizeWhere it livesWhy there
search_events≈ 30 GB a dayhourly log files in a blob storeappend-only; read only by batch jobs
query_dailyone row per query per day, most of them rarea table the batch job appends tolets each build add one hour instead of rereading 30 days
trie_snapshots≈ 15 GB per version, all languagesversioned files in a blob storeservers download the whole file
blocklistthousands of rowsa small database, also pushed to serverschanges must apply within minutes
search_events

Size≈ 30 GB a day

Where it liveshourly log files in a blob store

Why thereappend-only; read only by batch jobs

query_daily

Sizeone row per query per day, most of them rare

Where it livesa table the batch job appends to

Why therelets each build add one hour instead of rereading 30 days

trie_snapshots

Size≈ 15 GB per version, all languages

Where it livesversioned files in a blob store

Why thereservers download the whole file

blocklist

Sizethousands of rows

Where it livesa small database, also pushed to servers

Why therechanges must apply within minutes

user_hash is a one-way hash of the user id, made with a secret key so nobody can match it back by hashing known ids. The build needs to count how many different people typed a query, so it never suggests something only a handful searched for, but it never needs to know who they were.

The high-level design

With the data split in two, the system splits the same way. The online path answers keystrokes from caches and memory. The offline path turns logs into a fresh trie every hour and ships it to the servers as one file. Follow the online path first.

Animated sceneKeystrokes to suggestions0:24

Neha types "i". The edge in her city already holds this prefix

Read it as steps

Short prefixes are answered at the edge; rarer ones go to a suggestion server that answers from memory. A crashed server is replaced by loading a file.

Three lessons. Many keystrokes never leave Neha's city, because short prefixes are shared by everyone and stay cached at the edge, as in the CDN chapter. A miss costs extra network hops but no disk or database work. And a failed server is replaced by copying a file, because suggestion servers hold no state of their own.

That is the offline path, the second deep dive. The contract between the two paths is the snapshot file, plus two small lists covered later: trending queries and blocked ones. As long as a valid snapshot exists, the online path keeps working, even if the build pipeline is down for a day.

The phone's share of the work

The diagram starts at the phone, and the phone decides two things: how many requests reach the edge, and which answers reach the screen. Remember Neha's list jumping back to in. Four habits fix both.

  • Keep one request in flight. While an answer is on its way, send nothing more; when it returns, send only the newest prefix. A fast typist skips the prefixes in between, and a slow typist still gets an answer for every key. If an answer takes longer than about 300 ms, stop waiting and send the newest prefix anyway.
  • Debounce lightly, if at all. Debouncing means sending only after typing pauses for a set time. A 150 ms pause would skip most keys but add 150 ms to every answer, more than the whole budget; one request in flight saves requests without that delay.
  • Drop stale answers. An abandoned request can still answer late, so number every request and show an answer only if it is newer than the list on screen.
  • Reuse what you have. Filter the list for ind on the phone to show something for indi while the real answer loads, and keep recent answers in memory so backspacing touches no network at all.
Animated sceneA late answer, dropped by its number0:15

Watch for request #1: on a weak network its answer for "in" is slow

Read it as steps

A slow answer for "in" would pass a prefix check after the user has typed "ind", but its request number is older, so the phone drops it.

The rule is "newest request wins", not "matching prefix wins". It costs one counter on the phone, and it is what stops the list jumping back to an older answer while the user types.

Part 3 · Deep Dives

A trie with answers at every node

Core deep dive

The heart of the serving path: a stored top 10 at every node turns millions of matches into a 1 ms lookup.

A server receives ind and must return 10 queries in about a millisecond. Search results come from an inverted index (the distributed search chapter), which matches whole words. Suggestions must match the start of a query, and the classic structure for that is a trie (said like "try"): a tree in which each edge adds one character. Every node stands for a prefix, and every query below it starts with that prefix, so reaching ind takes three steps however many queries are stored. Think of a library shelved by the first letters of each title: finding the shelf is quick, but the shelf for "i" is enormous.

Predict before you watch: a short prefix such as "i" has millions of queries below its node. If nodes store nothing but children, what does each keystroke cost? What would you store at each node to make it cheap?
  • A walk to the node, then a scan and sort of everything below it: millions of queries for "i", on every keystroke.
  • Store each node's top 10 query ids, filled offline from its children; a lookup becomes a short walk plus one read.
Animated sceneWhere the answer for "ind" comes from0:18

Naive: walk to "ind", then visit every query below it and sort them

Read it as steps

A naive lookup scans everything below the prefix on every keystroke. Lists filled offline, bottom-up, turn it into a short walk and one read.

The lesson: precompute the answer. Finding the best 10 below a prefix happens once per build, not once per keystroke, and the stored list is only 40 bytes.

python
def suggest(trie, prefix, k=10):
    node = trie.root
    for ch in prefix:                 # one step per character typed
        node = node.children.get(ch)
        if node is None:
            return []                 # no kept query starts with this prefix
    return [trie.text[qid] for qid in node.top[:k]]   # precomputed list

A plain trie; a compressed trie compares a whole edge label per step.

Filling the lists and shrinking the trie

The scene filled the lists from the bottom up. Precisely: a node's top 10 is the best 10 among the query that ends at that node, if any, and its children's top 10 lists. One pass over the tree fills every node, offline, so its cost never touches a keystroke. Here is the result for six queries, keeping the top 3 to fit the page:

Six queries and their scores
QueryScore
india vs australia900
indigo flight status700
indian railway pnr650
india post tracking400
inox movies300
indore weather120
Top 3 stored at some nodes
PrefixStored top 3
inindia vs australia, indigo flight status, indian railway pnr
indiindia vs australia, indigo flight status, indian railway pnr
indiaindia vs australia, indian railway pnr, india post tracking
indoindore weather
inoinox movies

Four tricks keep the trie inside the 15 GB estimated earlier. Store 4-byte query ids in each list, not the text: about 16 bytes saved per entry. Compress the trie: merge chains of single-child nodes where no query ends into one edge, so there are at most 2 nodes per query instead of one per character; a prefix that ends mid-edge uses the list of the node the edge leads to, since the same queries lie below both. Keep 10 entries per node, not 50, because the list is the biggest part of each node. Drop queries few people typed, the huge long tail that is rarely useful.

A flat table works too

Since a lookup never looks below the prefix's node, the trie can be flattened into a key-value table: prefix → top 10. It takes several times more memory, because every distinct prefix becomes its own row, but it is easy to shard and keep in an ordinary key-value store. That is the usual next step when one copy no longer fits a server.

From search logs to scores

The trie is only as good as the scores in its lists, and those come from real searches. Every submitted search is logged as an event, about 30 GB a day. Each hour a batch job normalises the new hour's queries (lowercase, collapsed spaces, one Unicode form, so equal queries count together) and adds them to a count per query per day. It never rereads 30 days of raw logs.

Scoring with decay. Raw counts would favour old hits forever. Instead, each day's count is multiplied by a weight that halves every 7 days, a 7-day half-life. Say a query about tonight's final is searched 1,000 times today: it scores 1,000. Last month's match, searched 10,000 times four weeks ago and not since, has been halved four times: 10,000 ÷ 16 ≈ 625. Tonight wins, as it should. Days older than 30 weigh under 5%, so the job stops there.

python
HALF_LIFE_DAYS = 7

def score(daily_counts, today):
    # daily_counts: {date: count} for one normalised query
    total = 0.0
    for day, count in daily_counts.items():
        age_days = (today - day).days
        total += count * 0.5 ** (age_days / HALF_LIFE_DAYS)
    return total

A small bonus for suggestions people tapped, and a small penalty for ones they skipped, also helps.

Counting different people. The privacy rule keeps a query only if at least 50 different people typed it. Daily counts of distinct users cannot simply be added up, because Neha searching on three days would count three times. So the job keeps a small mergeable sketch per query per day, such as a HyperLogLog, which estimates distinct users within a few percent; merging 30 daily sketches gives the 30-day figure. Keeping sketches only for queries searched more than a handful of times that day keeps their storage small, at the cost of missing a few slow, steady queries.

Build, check, ship and swap

Scores in hand, the job builds a compressed trie per language, fills its lists bottom-up and writes it as one versioned file. Three steps stand between that file and Neha's screen, and each one prevents a specific failure.

  • Write the file in the memory layout. If the file holds the same packed arrays the server uses, a server maps it into memory instead of parsing 200 M nodes, so a swap costs a download and seconds of work, not minutes.
  • Validate before publishing. A bug that produces an empty or half-built trie would blank every search box at once. Compare each build with the last one (query count, file size, answers for a list of common prefixes) and refuse it if they differ wildly.
  • Canary, then swap. One server loads the new file first and takes a little traffic; if its error rate and acceptance rate look normal, the rest follow. Each maps the new file beside the old one and switches a single pointer; requests in progress finish on the old trie. That is why the memory estimate planned for two copies.
Animated sceneA broken build never reaches a server0:15

Watch for the check that stops a broken trie before any server loads it

Read it as steps

The hourly build is checked against the last one, then tried on one canary server, before every server swaps it in.

Shipping is cheap. Each server fetches about 15 GB an hour, about 12 seconds over a 10 Gbps link, so 9 servers move about 135 GB an hour from the blob store. Rolling back is pointing at the previous file. Hundreds of servers would call for staggered downloads or shipping only the changed parts.

Fresh, personal and safe

An hourly trie with decayed scores covers most needs. Three gaps remain: news faster than an hour, what this user personally wants, and what must never appear.

Trending queries. An hourly build is too slow when a match ends on the last ball. Add a small fast layer: a streaming job counts queries over the last 10 to 15 minutes, spots those rising far above their usual rate, and publishes a short trending list per prefix every few minutes. It passes the same safety filters, and servers merge it with the trie's answer at request time.

Personal suggestions. Your own recent searches are the best predictor of what you will type next. Keep the last 50 or so on the device, match them locally and show them above the shared list. The shared response stays the same for everyone, so the edge can still cache it.

Language and place. Build one trie per script and language, and pick it by what is being typed: many people type Hindi words in English letters, and those queries belong in the Latin-script trie. Where suggestions differ by place ("weather" should complete to the user's own city), add a coarse region such as the state to the request and the cache key. Keep regions few, because each one splits the edge cache.

Never suggestEnforcedWhy
Slurs, hate and sexual termsat build time, plus a serve-time emergency lista suggestion reads like the product speaking
Phone numbers, emails, ID numbersat build time, by pattern matchingpersonal data typed into a search box
Queries typed by fewer than 50 different peopleat build timea rare query may contain one person's private details
Completions removed for legal reasonsserve-time list, live within minutescannot wait for the next hourly build

The serve-time list is the safety valve: an entry pushed to every server, plus a purge of the affected prefixes from the edge caches, removes a bad suggestion within minutes. The build filter is fixed afterwards.

Part 4 · Failures and Trade-offs

What breaks, and how to cope

Typeahead has one rule above the rest: suggestions may fail, the search box may not. If anything below goes wrong, the app shows no list and the user types and searches as usual.

FailureWhat users seeHandling
A suggestion server crashesnothinghealth checks remove it; every server has a full copy
A zone or region failsslightly slower listsservers run in several zones; when a region is lost, edges send misses to the next region, a few tens of ms farther, and the 6 servers left absorb 40,000 a second
The build pipeline stopssuggestions slowly go staleservers keep the last good trie; alert when it is over 3 hours old
A bad build is producednothing, if checks holdvalidation refuses it; a canary catches what validation misses; roll back to the previous file
An offensive suggestion slips througha screenshot on social mediaserve-time blocklist and an edge purge within minutes, then fix the build filter
A big match, 5 to 10× for minutesmore edge misses for new prefixeskeep headroom: a new server must fetch and map 15 GB before it helps

Hot prefixes are a gift here, not a problem. In most systems a hot key hurts. In typeahead the hottest prefixes are one or two letters long, and there are only about 700 of those with the 26 English letters, so the edge caches them almost perfectly.

Sharding, when you need it. If many more languages arrive or a trie outgrows one server, split by prefix range (for example, a to c on one group of servers), with ranges sized by measured data, since far more queries start with s than with x. Keep one- and two-letter prefixes in a small table on every server, so the shortest prefixes never depend on the shard map.

Trade-offs, cost and what to watch

Most choices in this design trade one thing: fresher and more personal results cost cacheability and simplicity. The table shows where each choice landed.

DecisionOption AOption BUsually pick
Freshnesshourly batch buildfully streaming updatesbatch, plus a small trending layer
Index formtrie in memoryprefix table in a key-value storetrie while it fits in RAM; a sharded trie or a table beyond that
Top 10 listsstored at every nodecomputed per requeststored, unless memory is very tight
Personal resultsmerged on the servermerged on the devicedevice, to keep responses cacheable
Client requestsone per keystrokenewest prefix only, one in flightnewest only, with local filtering
Freshness

Option Ahourly batch build

Option Bfully streaming updates

Usually pickbatch, plus a small trending layer

Index form

Option Atrie in memory

Option Bprefix table in a key-value store

Usually picktrie while it fits in RAM; a sharded trie or a table beyond that

Top 10 lists

Option Astored at every node

Option Bcomputed per request

Usually pickstored, unless memory is very tight

Personal results

Option Amerged on the server

Option Bmerged on the device

Usually pickdevice, to keep responses cacheable

Client requests

Option Aone per keystroke

Option Bnewest prefix only, one in flight

Usually picknewest only, with local filtering

Each pick flips under a condition: a product where freshness within seconds matters, such as live scores inside the app, justifies streaming updates; a product whose index is far larger than RAM, such as every address in the country, starts as a sharded table.

What dominates the bill: the request count. 2.4 B requests a day is about 72 B a month, and edges price or provision per request, while the data is small and nine 64 GB servers answer the misses.

Part 5 · Interview kit

The 45-minute answer

A 45-minute round leaves about 40 minutes of design. Here is typeahead on that clock.

MinutesWhat you say and drawKey number or decision
0–5Requirements: up to 10 completions per prefix, popular and recent first, own recent searches, nothing offensive or private. Out of scope: spelling, the results page.About 100 ms from keystroke to list
5–8Estimates that decide something: requests per second, edge misses, trie memory.28,000/s average, plan for 100,000/s; about 15 GB per copy
8–13API: GET /v1/suggest with Cache-Control, POST /v1/events. Data: events, daily counts, trie snapshots, blocklist.The same answer for everyone, so it is cacheable
13–22Online path: phone, edge cache, load balancer, servers with the whole trie in memory. Offline path: events, hourly build, validation, snapshot file, swap.One copy fits in RAM: add servers for throughput, not space
22–37Deep dive: the top 10 at every node, filled bottom-up; then decay scoring, validation, canary and swap.About 1 ms per lookup; 7-day half-life
37–40Wrap-up: a bad build, a lost region, a big match; personal results on the device; sharding by prefix.Suggestions may fail; the search box may not

Pick the trie if they ask how a lookup stays near 1 ms with millions of queries. Pick the build pipeline if they ask how suggestions stay fresh, ranked or safe. If they ask about personal results, explain the merge on the device and why it keeps the edge cache working.

Say
"About 8 requests per search makes 100,000 a second at peak, inside a 100 ms budget that is mostly network, so short prefixes are answered at the edge."
Say
"Each trie node stores its top 10 query ids, so a lookup is a short walk plus one read, about 1 ms; the price is an hourly offline build."
Say
"The trie is about 15 GB in a packed layout, so every server holds a full copy; personal results merge on the device so the shared answer stays cacheable."
Avoid
"I will run LIKE 'ind%' on the search database for every key." A short prefix matches millions of rows that must be sorted by score, on every key, at 100,000 requests a second.

Interview drill

Each question changes one thing. Answer it before you open it.

A cricket final ends and suggestion traffic jumps 10× for 15 minutes, everyone typing the same few new queries. What happens?
  • Those prefixes are shared, so after the first miss in each city the edge serves them; servers see a short burst of misses.
  • The new queries are not in the hourly trie yet; the trending layer adds them within 10 to 15 minutes.
  • Keep headroom rather than counting on autoscaling: a new server must fetch and map 15 GB first.
  • If servers saturate, return empty lists fast: the search box still works.
Product wants pnr to suggest "indian railway pnr status", matching the start of any word. What changes in the data?
  • Insert each query once per word start, every copy pointing to the same query id, with a lower score for matches after the first word.
  • At about 3.5 words per query that is 350 M entries, about 45 GB a copy and 90 GB during a swap: past a 64 GB server.
  • Index word starts only for the top 20 M queries (about 20 GB a copy), or shard by prefix.
You must cut the serving cost in half. Where do you look?
  • Requests are the bill, so send fewer: a node whose list has under 10 entries holds every completion below it, so the phone filters longer prefixes itself.
  • Cache one- and two-letter prefixes for longer than 5 minutes, since they change slowly.
  • A 30 to 50 ms debounce for fast typists cuts requests further, at a small latency cost.
Common miss
A personal field in the shared response. One per-user value makes every answer uncacheable at the edge.
Common miss
Updating the trie on every search. Fresh data belongs in the hourly build and the trending layer, never in the request path.

Wrap-up

Next, Design Collaborative Document Editing keeps a live connection open for every editor and merges many people's keystrokes into one document.

Part 6 · Check yourself

Quick check

Q1Users in one region see yesterday's suggestions while other regions show tonight's trends. Search works. What do you check first?

Think it through, then check.

Q2Product wants personal suggestions computed on the server, so a search on Neha's laptop shows up on her phone. What changes?

Think it through, then check.

Q3A teammate says: skip the trie and run LIKE 'ind%' on the search database, because it is always fresh. What does that freshness cost?

Think it through, then check.

Q4The hourly build has been refused by validation for 12 hours on a day of big news. What do users see, and what do you do?

Think it through, then check.

Q5Which building block answers most keystrokes, and why can it do that here when it cannot for most personal APIs?

Think it through, then check.

Next chapter · 34 of 39Design Collaborative Document Editing~40 min · 2 animated scenes