High-Level Design and APIs
In one line: the split into two services is the whole architecture, and the reason is the latency budget from Lesson 1.
The two functions
The system should perform two main functions:
- Provides suggestions based on user search history.
- Stores new and trending queries in the database to update the suggestion list.
When a user begins typing, each keystroke triggers a request. A suggestions service retrieves the top 10 suggestions from a Redis cache and returns them. The system also includes an assembler service, which collects search queries, processes them to compute rankings, and stores the results in a distributed NoSQL database.
Two services because they run on clocks four orders of magnitude apart
This is the same split the newsfeed chapter made between generation and publishing, and here the gap between the two clocks is larger than anywhere else in the module:
| Suggestion service | Assembler | |
|---|---|---|
| Triggered by | Every keystroke | A schedule |
| Budget | Under 200 ms | Minutes |
| Work | A lookup | Aggregate, rank, rebuild |
| Rate | ~607,000/second | Every 15 minutes |
| If it fails | Users see nothing | Suggestions go stale |
Read path: 200 ms
Write path: 15 minutes
-------------
~4,500x apart
That ratio is what makes the design work. Lesson 1 established that a late suggestion is a wrong suggestion, so nothing expensive may sit on the read path. Everything expensive therefore moves to a service that answers to a schedule rather than to a user.
Split components by their latency budget, and here the budgets differ so sharply that the two halves share almost nothing — the suggestion service touches only Redis; the assembler touches HDFS, MapReduce, Cassandra, and MongoDB.
Note the failure asymmetry too. If the assembler stops, the system keeps working and simply serves increasingly stale rankings. That is a good property: the component doing the hard work is the one whose failure is least visible.
Two clocks. The query path is measured in milliseconds; the build path in hours. Keeping them separate is what lets the read path do no work — the expensive part already ran.
The APIs
getSuggestions(prefix) addToDatabase(query)
| Parameter | Description |
|---|---|
prefix | Whatever the user has typed in the search bar |
query | A frequently searched query that crosses the predefined limit |
getSuggestions takes exactly one parameter, and that is the point
No user ID. No session. No context. Just a prefix.
That parameterlessness is what makes the response cacheable across all users — a prefix maps to the same ten suggestions no matter who typed it, so a single cache entry serves everyone, and Lesson 8's edge caching becomes possible.
Compare the newsfeed and Instagram chapters, where every response was personalized by construction and therefore uncacheable at a shared edge. Here the opposite holds, and it is worth stating as a design property:
An impersonal API is a cacheable API.
Which sets up a real tension. Lesson 8's evaluation says suggestions "should incorporate the user's search history, location, and language preferences" — and the moment getSuggestions takes a user ID, the shared cache stops working. Lesson 8 covers the contradiction.
Two things the signature also lacks:
No result count. The system returns ten because ten is hardcoded, not because the caller asked.
No language or locale. Which matters more than it sounds — "uni" should suggest different things in different languages, and the parameter list has no way to express it.
addToDatabase fires 'only if the query exceeds a threshold', which is backwards
This API adds a trending query to the database via the assembler. This occurs only if the query has been searched previously and exceeds a specific threshold.
That describes a filter applied before the assembler sees the query. But the assembler's entire job, per Lesson 7, is to count frequencies — and you cannot know whether a query exceeded a threshold until you have counted it.
As described: check threshold -> then log Necessarily: log everything -> then count -> then threshold
Lesson 7's actual design confirms the second: the collection service logs every phrase with a timestamp to HDFS, and a MapReduce job computes frequencies afterwards. There is no threshold at ingestion.
So the API description contradicts the pipeline it feeds. The workable reading is that addToDatabase is not a filter but a log write, and thresholding happens downstream at aggregation time.
It matters because the difference changes what you can detect: filter at ingestion and you can never discover a query that is newly trending, since it has no history to exceed a threshold with. Rising queries are exactly the ones a typeahead system most needs to catch.
A frequency threshold cannot be applied before frequencies are counted.
AJAX, and what it stands in for
The design's own question — how to update only the suggestion box rather than the whole page — is answered with AJAX: exchanging a small amount of data with the server "without interfering with the display and behaviors of the existing web page."
Correct, and dated. The modern equivalent is fetch with the suggestion list rendered by the client framework, and Lesson 8 mentions WebSockets as an option — "establish a connection as soon as the user visits the search page."
The WebSocket suggestion is worth taking seriously against Lesson 1's budget. A fresh HTTPS request costs a TCP handshake plus a TLS handshake — potentially two or three round trips before a byte of payload moves. At a 200 ms budget over a mobile connection, that alone can consume it.
A persistent connection removes handshake cost from a budget that cannot absorb it — the same argument that building block made for a different reason.
Key takeaway
The design splits into two services whose clocks are roughly 4,500× apart — 200 ms against 15 minutes — and they share almost nothing, which is what keeps expensive work off a path where a late answer is a wrong one. The assembler's failure is also the least visible, degrading freshness rather than availability. getSuggestions(prefix) takes exactly one parameter, which makes the response cacheable across all users — an impersonal API is a cacheable one, and personalization later breaks it. And addToDatabase's threshold cannot work as described: you cannot filter on frequency before counting it, and filtering at ingestion would make newly-trending queries permanently undiscoverable.
Next: the trie.