SwadhyAI Notes

All System Design notes

System Design Case Study

Typeahead / autocomplete

Type "how to" into Google Search or YouTube. Before the next letter, a list of likely queries appears under the box. To the user it is one helpful list. Underneath, every keystroke can become another request that must finish in tens of milliseconds.

Typeahead predicts what the user means before they finish typing. Most intelligence is precomputed offline; the online path only walks a , returns , and optionally merges a tiny .

Keystroke → suggestions

1

Wait ~100ms; cancel stale requests.

2

Follow the typed prefix in memory.

3

Precomputed completions on the node.

4

History + trending, then safety filter.

Try it: typeahead prefix lookup

Each keystroke queries a prefix index. Caching top prefixes and limiting fan-out keeps p99 low.

  1. Client
  2. API
  3. Prefix cache
  4. Trie / index
  5. Ranker
Highlighted hops show where the last action moved traffic. A stopped hop means backpressure, rejection, or a consistency guard blocked further progress on that path.
Queries
0
Cache hits
0
Mode
cold
Path hop
0

Latency

prefix

0 queries, 0 cache hits.

Fan-out

limit

Bound result cardinality; never stream unbounded matches.

Freshness

index

New trending terms lag until the suggestion index refreshes.

Type characters against a cold index, then warm the prefix cache.

What actually happens

Search logs feed an : aggregate frequencies, filter unsafe queries, build a versioned with at each node, then deploy with blue-green or pointer swap. The online service does not rebuild that structure on every query.

On each keystroke (after ), the server walks the typed , reads the cached suggestion list, merges and , and filters again for policy blocks. Short are replicated everywhere and cached at the edge.

If personalization fails, return global safe suggestions. If a new index is bad, roll back the version pointer: do not leave users staring at empty dropdowns.

How to choose the typeahead shape

1. Where does ranking intelligence live? Put frequency and safety into the . Keep the online path a fast walk plus merge.

2. Will short prefixes melt a shard? Treat one- and two-character as a special case: replicate and CDN-cache them.

3. Is personalization private? Scope history overlays with TTL and keep them out of shared caches so one user's query never leaks.

Simple rule of thumb

Design for sub-100ms reads first: in-memory , , multi-layer cache, versioned rebuilds. Add personalization and trending only after the global path is solid.

In simple terms: do the heavy thinking offline; online should feel like a dictionary lookup.

Case-study upgrade

Typeahead Prefix Math

Typeahead should answer from a prebuilt prefix index. Online work should be bounded by prefix length, not by corpus size.

Assumptions and formulas

  • Searches: 5B/day. Suggestion requests/search: 5 before debounce.
  • Each response returns 10 suggestions and a small envelope.
  • One-letter and two-letter prefixes are hot and should be replicated widely.

Concrete math

  • Typeahead requests: 5B searches/day x 5 = 25B/day = 289K QPS average.
  • Peak: 2x to 5x = 580K to 1.45M QPS.
  • Response bandwidth: about 700 bytes/response. At 580K QPS, egress is 406 MB/sec before compression.
  • Prefix index: 10M prefixes x (10 suggestion ids x 16 bytes + 40 bytes metadata) = about 2 GB raw; with strings and overhead, plan 5 to 20 GB.
  • Debounce from 5 to 2 requests/search cuts average QPS from 289K to 116K.

Design decisions forced by numbers

  • Build versioned indexes offline and swap serving pointers after validation.
  • Replicate hot prefixes and cache negative/empty prefixes briefly.
  • Personalization should rerank a small candidate list, not search the whole corpus online.

Scaling to millions

  • At 1M users: one prefix index in memory is enough.
  • At 10M users: add shards, hot-prefix replication, and client debounce.
  • At 100M+ users: add regional serving, version rollback, privacy filters, and per-prefix metrics.

Readable flow

Query logs
Aggregation
Index build
Canary
Pointer swap
Serving cache

Interview follow-ups to expect

  • How do hot prefixes avoid overloading one shard?
  • How do we roll back a bad index build?
  • How much personalization can happen within the latency budget?

Operational readiness checklist

After the math, explain how this capacity plan is measured, protected, launched, and evolved.

Observability

  • Attach each capacity estimate to a dashboard metric: p99 latency, queue age, cache hit rate, shard skew, error rate, and freshness lag.
  • Alert on rate-of-change and saturation, not only hard failures; backlogs and hot keys usually warn before an outage.

Security and abuse

  • Budget for auth checks, rate limits, quota isolation, audit logging, privacy filters, and abuse-review side paths.
  • Separate trusted internal events from user-controlled input so retries and backfills cannot bypass policy checks.

Degraded modes

  • Define the fallback when a dependency is slow: stale cache, partial results, read-only mode, delayed async work, or admission control.
  • Protect the invariant first; reduce freshness, personalization, or analytics quality before risking duplicate or corrupt state.

Testing and rollout

  • Test average, peak, and hot-object traffic; include retry storms, cold caches, lagging workers, and regional dependency loss.
  • Use canaries, shadow reads, dual-write validation, feature flags, and explicit rollback criteria for risky path changes.

Rollout, backfill, and runbooks

  • Make migrations and backfills idempotent, resumable, rate-limited, and visible through progress and error metrics.
  • Write the runbook before launch: owner, dashboards, paging thresholds, mitigation switches, replay steps, and repair commands.

10x evolution

  • For typeahead prefix math, state which number forces the next move: cache memory, shard count, write amplification, queue lag, egress, or failover reserve.
  • Add the next layer only when the metric forces it: sharding, regional reads, async replay, hot-key isolation, or stricter backpressure.

Scope you should nail down

Product surface

  • Real-time suggestions as user types
  • Top K suggestions (e.g., 10 per keystroke)
  • Personalized suggestions based on user history
  • Support trending queries and recency
  • Multi-language and Unicode support
  • Filter inappropriate suggestions

Scale targets

  • Latency <100ms (p99)
  • 5B searches/day
  • 10 suggestions per keystroke
  • High availability 99.99%
  • Handle 10M+ unique query prefixes

Capacity estimation

Assume 5 chars typed before selection = 5 typeahead requests per search.

290K
QPS (peak 580K)
25B/d
Typeahead requests
10M
Unique prefixes
2-5 GB
Trie in-memory

Hardening prefix serving

Capacity formulas and SLOs

5B searches/day x 5 keystrokes = 25B requests/day = 289K QPS average, 580K-1.45M QPS peak. Target p99 under 100ms, index age under the freshness SLO, and zero-result rate within normal bounds by locale.

Metrics and degraded modes

Track cache hit rate by prefix length, hot-prefix QPS, serving p99, build duration, bad-suggestion reports, click-through rate, and personalization overlay latency. If personalization fails, return global safe suggestions.

Safety and privacy

Filter unsafe suggestions at build time and again at serve time for urgent policy blocks. Keep user history overlays scoped, TTL-bound, and excluded from shared caches so one user's private query does not leak to another.

Rollout, replay, and 10x growth

Build versioned indexes from replayable logs, canary by locale, keep previous pointer for rollback, and backfill trend overlays independently. At 10x, replicate one- and two-character prefixes to every serving node.

Hard-part drill: prefix hot keys

Prefixes like "a" and "ne" receive a large share of traffic. Serve them from browser/CDN cache, replicate them on every typeahead node, avoid routing by first character alone, and keep personalization as a tiny local overlay so the global hot prefix stays cacheable.

3. 🔌 System API

getSuggestions API

typescript
getSuggestions(prefix: string, user_id?: string, limit?: number): Suggestion[]

  prefix: partial query string (e.g., "how to")
  user_id: optional for personalization
  limit: number of suggestions (default 10)

Returns: [{ query: string, frequency: number }, ...]

4. 🌳 Trie Data Structure

Prefix Tree

A trie groups strings by shared prefixes. Inserting “car”, “card”, and “cat” creates a shared path for “ca”, then separate branches. To answer “ca”, follow two character transitions and read a precomputed list of its best completions. Without that list, the service would have to explore the entire subtree and rank all matching strings on every keystroke.

With bounded transition lookup and precomputed lists, traversal takes O(prefix length), followed by O(K) to return K suggestions. Storing K suggestions at every node improves reads but duplicates suggestion references throughout the index. Account for that memory cost, and use sparse child maps or compressed paths where most possible characters are absent.

TrieNode structure

rust
struct TrieNode {
  children: Map<char, TrieNode>
  isEnd: bool
  frequency: int
  topSuggestions: List<Suggestion>
}

Memory Optimization

A fixed 26-child array works only for a restricted alphabet and wastes slots at sparse nodes. A sparse map stores existing branches; a radix tree compresses a sequence of single-child nodes into one labeled edge. For example, a unique suffix “ography” can occupy one edge rather than seven separate nodes.

Compression savings depend on the actual query vocabulary, language, and top-K payload. Measure bytes per indexed query on a representative dataset instead of assuming a fixed 50% saving. Large suggestion lists can dominate memory even after the trie structure is compressed.

5. 📥 Data Collection Pipeline

Flow

Search queries → aggregate frequency → update trie offline. Do NOT update trie on every query (too slow). Batch with MapReduce.

  • Log search queries to stream log
  • MapReduce: aggregate (query, count) per day
  • Trie Builder: rebuild trie from aggregated data
  • Deploy new trie every N hours (e.g., 6–24h)

Why Offline

Offline rebuilding is a choice for predictable serving latency, not a claim that tries cannot be updated online. A query event can change counts for many prefixes and force their top-K lists to be recomputed. Aggregating these events into a new immutable index separates that work from user requests and makes rollback straightforward.

For example, publish a full index every six hours and keep a small streaming overlay for fast-moving trends. Merge the overlay with the base index at read time. This gives popular breaking queries a path into suggestions without continuously mutating the entire serving structure.

6. 🏆 Ranking Suggestions

Factors

  • Frequency: how often query was searched
  • Recency: recent queries boost
  • Personalization: user history overlay
  • Trending: spike in last hour/day
  • Location: geo-specific suggestions

Implementation

Compute global suggestion scores from normalized frequency and recency, then keep a larger candidate set than the final ten suggestions. At request time, merge a bounded set of personal-history and trending candidates, deduplicate them, filter policy violations, and select the final K. Keeping exactly K before filtering can produce a short or empty list when several candidates are removed. Evaluate ranking changes by locale and prefix length, because an overall click-through increase can hide worse behavior for smaller languages.

7. 🏗️ Architecture

Client (debounce 100ms)
↓
API Gateway
↓
Typeahead Service
↓
Trie (in-memory)In-memory Cache

Offline Pipeline

Query logs → stream log → MapReduce → Trie Builder → Deploy to Typeahead servers. Blue-green deployment for zero-downtime trie updates.

8. 🗂️ Trie Sharding

Prefix Range

Shard by first char: a-f on server 1, g-m on server 2, n-s on server 3, t-z on server 4. Client or LB routes by prefix.

Consistent Hashing

Hashing materialized prefix records can distribute ownership more evenly than assigning the entire letter “s” to one server. A record maps a normalized prefix and locale to a precomputed suggestion list, so the query routes to one shard. This is different from hashing complete query strings: the completions for “ca” would then be spread across many shards and require a fan-out query. Hot prefixes still need replication or caching because hashing cannot divide one record’s traffic.

9. 💾 Caching

Browser

Cache the global result under the normalized prefix, locale, index version, and any other input that changes its contents. Choose a TTL from freshness requirements; one hour may suit stable catalog names but miss a rapidly emerging news query. Browser history-based suggestions contain personal data and should use private storage and clear retention rules rather than shared HTTP caches.

Content Delivery Network (CDN)

Cache popular prefixes at edge. Cache at multiple prefix lengths (1, 2, 3+ chars).

Application

in-memory cache store for hot prefixes. Trie is already in-memory; in-memory cache store for personalization overlay.

10. 🌐 Multi-Language & Personalization

Unicode Trie

Use Unicode code points for children. Support CJK, emoji, etc. Separate tries per language or unified with language tag.

Personalization

User history overlay: merge trie suggestions with user:{user_id} recent queries from in-memory cache store. Boost user's past queries. A/B test personalization weights.

11. 🚫 Filter Inappropriate Suggestions

Apply policy checks while building the global index so most prohibited suggestions never enter serving memory. Also keep a fast current blocklist on the response path: an urgent takedown cannot wait six hours for the next rebuild. Filter before choosing the final K and retain spare candidates to fill removed positions.

Moderation must cover normalized variants, languages, and query logs that contain personal information. Ranking frequency is not evidence that a query is safe to publish. Version policy changes, measure rejection rates by locale, and provide a rollback path when a rule removes legitimate suggestions.

Client-Side Optimizations

Debouncing starts a request only after typing pauses for a short interval, such as 100–200 ms. This trades fewer requests for a small wait; the reduction depends on actual typing speed and should be measured instead of promising a fixed multiplier. A minimum prefix length can further reduce broad, unhelpful requests, but must suit the product and language.

Cancel obsolete requests where possible and also assign each request a sequence number. Cancellation may arrive too late, so the client must ignore a response for “ca” if the current text is already “cat”. Keep a small cache of recent prefixes and prefetch only when the likely benefit justifies extra requests; speculative prefetching can otherwise undo the load savings from debounce.

Trie Deployment Strategy

Blue-green deployment: build new trie on standby servers, atomic swap via config or load balancer. Zero-downtime. Rollback: keep previous trie version. Canary: route 1% traffic to new trie, monitor latency and error rate.

12. ⚠️ Bottlenecks

Hot prefixes

Single-letter prefixes (e.g., "a") hit one shard. Mitigation: cache aggressively, Content Delivery Network (CDN).

Trie rebuild latency

Rebuilding takes time. Mitigation: build in background, atomic swap.

Memory per server

Full trie ~5GB. Mitigation: shard by prefix, compress trie.

Thundering herd

Cache miss on popular prefix. Mitigation: single-flighter, pre-warm cache.

Review the design

Trie with top-K at each node for O(prefix_length) lookup.

Offline pipeline: query logs → MapReduce → Trie Builder → Deploy. Keep aggregation work away from the latency-sensitive read path; a bounded streaming overlay can supply fresher trends.

Ranking: frequency, recency, personalization, trending. Pre-compute + user overlay.

Shard by prefix (a-f, g-m, etc.) or consistent hashing.

Multi-layer cache: browser, CDN, and in-memory cache store. Debounce the client.

Filter inappropriate suggestions at build or query time. Support Unicode trie behavior for multi-language search.

Compare a starting answer with a more complete design

A starting answer

Use a trie or prefix index, cache hot prefixes, debounce clients, and rank suggestions by popularity.

Developing the reasoning

Cover FSTs, versioned index rebuilds, hot short-prefix replication, personalization overlays, trend injection, bad-suggestion rollback, and click feedback loops.

Deeper walkthrough

Typeahead deeper walkthrough

Typeahead is an online latency problem backed by offline aggregation. Most intelligence should be precomputed; online logic should be fast and bounded.

Requirement refinements

Clarify data source: search queries, product names, users, documents, or locations, because ranking signals differ.

Define latency target, personalization scope, typo tolerance, language support, and freshness expectations.

API design details

Use prefix, locale, user context, limit, and experiment ids in request metadata.

Return suggestion id, display text, type, score, and tracking token for feedback attribution.

Data model and storage

Build versioned prefix indexes such as trie or FST from offline aggregation jobs.

Keep personalization and trending overlays separate so the global index does not rebuild per user.

Scaling strategy

Replicate hot short prefixes because one-letter and two-letter prefixes dominate traffic.

Roll out new index versions atomically and keep the previous version for rollback.

Reliability, recovery, and operations

If personalization fails, return global suggestions; if the new index is bad, roll back by version pointer.

Monitor suggestion p99, cache hit rate by prefix length, click-through rate, zero-result rate, and index build age.

Deepening reference

Typeahead / Autocomplete: realistic production notes

Beginner mental model: Typeahead is fast prefix lookup plus ranking, freshness, and personalization.

Production scope: A production system uses offline aggregation to build prefix indexes, online caches for hot prefixes, and overlays user/location/personal signals at query time.

Choose what the first version must guarantee

Start by deciding which typeahead / autocomplete capabilities are essential. Describe the main read and write operations before choosing infrastructure. For each operation, identify the stored records it uses and the condition that makes its result correct. A fast response is useful only if it satisfies that condition.

Next, separate work the user must wait for from work that can finish later. Estimate requests per second, storage growth, and the busiest object or customer. Use those estimates to choose one bottleneck to investigate. The calculations below are example workloads with stated assumptions; they are not measured limits of a particular product.

Core flow to walk through

1

Offline: aggregate queries and documents, build prefix trie/FST, compute top suggestions.

2

Online: receive prefix, fetch candidates from memory/cache, personalize lightly, return within budget.

3

Feedback: log impressions, clicks, and abandoned queries for next rebuild.

Production readiness

Typeahead / Autocomplete: ship, operate, and evolve

Now examine the same request flow during failures and deployments. Tie each operational control to the user behavior it protects.

Observability

Track typeahead / autocomplete p95/p99 latency, error rate, saturation, and user-visible freshness separately for read, write, and async paths.

Alert on leading indicators such as queue age, hot-key skew, cache miss storms, reconciliation mismatches, and error-budget burn.

Security and abuse

Enforce authentication, authorization, rate limits, input validation, audit trails, and privacy checks at the service boundary.

Treat abuse controls as a first-class path: throttling, quota isolation, moderation hooks, revocation, and safe handling of sensitive data.

Degraded modes

Name what still works if cache, search, ranking, stream processing, object storage, or a regional dependency is slow.

Prefer stale-but-safe reads, partial results, async retry, admission control, and feature disablement over corrupting core state.

Testing and rollout

Load test average, peak, and hot-object traffic; add failure-injection for retries, timeouts, backlog growth, and dependency outages.

Ship with canaries, shadow reads or dual writes when useful, rollback switches, schema compatibility checks, and replay validation.

Runbooks

Document dashboards, paging thresholds, owner actions, rollback steps, data repair commands, and customer-impact language.

Keep backfills and replays idempotent, resumable, rate-limited, and observable so repair work does not create a second incident.

10x evolution

Say which metric forces the next architecture change: shard count, cache footprint, write amplification, queue lag, p99 latency, or regional failover reserve.

Evolve one bottleneck at a time: partition by access pattern, isolate hot tenants, regionalize reads, add replayable async pipelines, then harden multi-region behavior.

Hard parts worth deepening

Sub-100 ms latency requires in-memory structures and aggressive caching.

Short prefixes are hot and need special cache treatment.

Index rebuilds should be versioned and rolled out atomically.

Personalization should be an overlay, not a full rebuild per user.

Failure modes and recovery

Trie rebuild publishes bad suggestions; keep previous version and rollback.

Hot prefix like "a" overloads servers; cache and replicate top prefixes.

Fresh trending term missing; add small real-time trending overlay.

Personalization service fails; return global suggestions.

Signals to monitor

Suggestion p99 measures whether results arrive before the user finishes typing. Cache hit rate by prefix length distinguishes popular short prefixes from less reusable, more selective long prefixes.

Click-through rate gives product feedback but can be influenced by placement and popularity. Index-build time measures update delay, and bad-suggestion reports identify inappropriate results that latency and click counts alone would miss.

ChoiceBest whenCost
TriePrefix lookup is central and memory is acceptable.Large memory and rebuild complexity.
FSTCompact prefix index is needed.More complex implementation.
Realtime updatesTrends matter strongly.Ranking instability and extra online complexity.

Try this scenario

Design how a new viral search term appears in suggestions before the next full trie rebuild.

Primary sources

Practice and further reading

Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.

Focus for this lesson: Serve normalized prefix candidates with typo tolerance, privacy thresholds, global/personal blending, cache invalidation, and zero-downtime index generations.

Explain the decision

Why is Unicode normalization part of typeahead?

Read the reasoning

Visually/equivalently represented text can have different code points; consistent normalization protects matching, dedup, and cache keys.

Apply it to a production scenario

A trending query contains private user data. What prevents leakage?

Read one approach

Apply aggregation thresholds, sensitive-term filters, differential/privacy policy where appropriate, audit sources, and rapid removal propagation.

Work through the numbers

Ten million prefixes average 120 bytes across three replicas. Estimate raw index memory.

Read the worked calculation

3.6 GB before trie/FST pointers, ranking features, allocator overhead, and generation overlap.

Reason through a failure

What must index rollout preserve?

Read the recovery reasoning

Every request uses one immutable generation; new data is validated before atomic pointer switch and the previous generation remains rollback-ready.