Lesson 7 of 8 · 55 min
Worked designs — rate limiter + search
Distributed token buckets without global locks, enforcement layers, inverted indexes, typeahead, async freshness, and ranking altitude.
Lesson 7 · Component designs
Rate limiter + search
Two deep-dives that show up everywhere
1import time23class TokenBucket:4 def __init__(self, rate_per_s: float, burst: float):5 self.rate = rate_per_s6 self.burst = burst7 self.tokens = burst8 self.t = time.monotonic()910 def allow(self, n: float = 1.0) -> bool:11 now = time.monotonic()12 self.tokens = min(self.burst, self.tokens + (now - self.t) * self.rate)13 self.t = now14 if self.tokens >= n:15 self.tokens -= n16 return True17 return False1819# 100 req/s sustained, burst 20020bucket = TokenBucket(100, 200)2122ALGORITHM TRADEOFF23Algorithm Pros Cons Pick when24--------------------- ---------------------- ----------------------- ---------------------25Fixed window trivial, 1 counter 2× boundary burst low-stakes / debug26Sliding log exact O(n) memory/key strict low-QPS quotas27Sliding window counter good approx, O(1) mem edge error general API limits28Token bucket burst + sustained two params API gateways default29Leaky bucket smooth egress adds latency outbound shapingDistributed rate limiting without a global lock
1-- Redis token bucket sketch (atomic via Lua in prod)2-- KEYS[1] = rl:{user}3-- refill based on rate; compare tokens; decr4-- Alternative simple fixed window:5INCR rl:user:123:20260715T12006EXPIRE rl:user:123:20260715T1200 607-- if count > LIMIT → 42989# Placement: edge (IP, coarse) → gateway (API key) → service (expensive ops)10# Return headers: Retry-After, X-RateLimit-Remaining when product allows11# Fail open vs fail closed if Redis is down — pick by abuse risk vs availabilityKey idea
Common mistake
“A global lock across all API servers is the safest rate limiter.”
Where to enforce (layered)
- 01Edge/WAF: IP and bot floods.
- 02API gateway: per-user/token policy.
- 03Service: protect expensive endpoints (search, export, fan-out).
- 04Downstream bulkheads: per-dependency limits so one client cannot melt Redis.
- 05Cost-based limits: expensive ops spend more tokens than cheap GETs.
- 06Nested buckets: per-user AND per-tenant must both allow.
Part B — Search with an inverted index
1# Tiny inverted index sketch (interview-scale)2from collections import defaultdict34class InvertedIndex:5 def __init__(self):6 self.postings = defaultdict(set) # term -> doc_ids7 self.docs = {}89 def add(self, doc_id, text):10 self.docs[doc_id] = text11 for term in tokenize(text):12 self.postings[term].add(doc_id)1314 def search(self, query):15 terms = tokenize(query)16 if not terms:17 return []18 sets = [self.postings[t] for t in terms]19 hits = set.intersection(*sets) if sets else set()20 return rank(hits, query, self.docs)2122def tokenize(text):23 return [t for t in text.lower().split() if t]Index build, freshness, sharding
1RANKING SIGNALS (altitude)2Signal Type Cost Staleness OK?3--------------------- ------------- ---------- ---------------4BM25 / lexical static low hours5Freshness time medium minutes6Popularity / CTR behavioral high hours7Personalization behavioral high days8Embedding similarity ML inference very high days910# Classic SWE: BM25 + business features. Deep LTR/embeddings → ml-system-design companion.Typeahead / suggest (full mini design)
1# Typeahead capacity → design (heuristic)2peak_qps = 30_0003# trie ~20–50 GB compressed class for large corpora → few boxes + replicas4# budget: ~5ms trie + ~5ms rank + network headroom inside 100ms p995# DECISIONS: in-memory structure, hot-prefix cache >95% hit, nightly rebuild + incremental counts6GET /suggest?q=pre&limit=10 → [{q, score}]Common mistake
“Search should always query the primary OLTP with LIKE %term%.”
Key idea
Interview answers — rate limit + search
- 01Where enforce limits? → edge coarse, gateway policy, service expensive ops.
- 02Distributed limits? → Redis atomic / sharded owners / local approx with overshoot stated.
- 03Fixed window vs token bucket? → burst tolerance → token bucket; simple debug → fixed.
- 04Redis down? → fail open (avail) or closed (abuse) by product risk.
- 05Per-user and per-tenant? → nested buckets; both must allow.
- 06Why Lua? → atomic check-and-decrement; no lost tokens under race.
- 07Cost-based limiting? → exports spend more tokens than GETs.
- 08Client headers? → Remaining / Reset / Retry-After.
- 09Shard search index? → by doc or tenant; scatter-gather top-k.
- 10Doc updates in inverted index? → new segment / tombstone old; eventual merge.
- 11Typeahead personalization? → blend only above confidence; else global.
- 12Measure quality? → NDCG/MRR/CTR offline + online interleaving.
- 13200ms-old write missing? → derived index freshness SLO, not always a bug.
- 14Deleted doc still appears? → hydrate filter + async purge postings.
Rate limit headers and client UX
Retry-After, remaining quota, and reset time when the product is a public API. Clients should backoff; SDKs should jitter. For browser apps, map 429 to user-visible “slow down” only when it is a user action, not for background polling (fix the poller). Document cost-based limits so a single export cannot surprise a customer who thought they had 1000 “requests.”Testing rate limiters
Search quality and spam
1SEARCH PIPELINE (interview board)2OLTP write → CDC/queue → analyzer → inverted index segments3Query → tokenize → retrieve postings → BM25 → (optional LTR) → hydrate SoT4Typeahead → trie/FST → popularity rank → cache56Freshness SLO example: 99% of docs searchable within 30s of commit7Delete SLO: deleted docs not returned after 60s (hydrate filter sooner)Cardinality at the limiter
rl:{customer}:{window} with TTLs so Redis memory stays bounded. Cloudflare-scale edge limiting (reported) leans on probabilistic structures when exact per-key state cannot fit. In interview, say when exact counters become impossible and approximate is OK for abuse defense.Checkpoint
You need a distributed per-user limit across 50 API nodes without a fleet-wide mutex. Best approach?
Checkpoint
A document was written 200ms ago; search does not find it. Is the system wrong?
Checkpoint
Token bucket vs fixed window — when is token bucket worth it?
Checkpoint
Search index returns a deleted doc. Best handling?
Checkpoint
Where should you rate limit a costly “export all messages” API?
End-to-end narration scripts (two 8-minute deep dives)
1RATE LIMIT DECISION LOG2Need Pick3--------------------------- ---------------------------4Burst + sustained token bucket5Simple debug quota fixed window (know 2× edge)6Exact low-QPS sliding log7Multi-node hard quota Redis/atomic or owner shard8Abuse-only soft limit local approx OK9Export expensive cost-based tokens1011SEARCH DECISION LOG12Need Pick13--------------------------- ---------------------------14Free text retrieval inverted index derived15Freshness seconds NRT segments + CDC16Prefix suggest <100ms in-memory FST/trie + cache17Deletes tombstone + hydrate filter18ML re-rank second stage / ML track1920Reported anchors: Stripe rate limiters; Cloudflare counting; Slack search hybrid.Rate limit + search decision cards
1RATE LIMIT PICKER2burst+sustained → token bucket (default API)3simple/debug → fixed window (warn 2x boundary)4exact low QPS → sliding log5multi-node hard quota → Redis Lua / owner shard6abuse soft → local approximate OK7expensive export → cost-based tokens8Redis down → fail open (avail) vs closed (abuse) by risk910PLACEMENT11edge IP/bot → gateway user/key → service expensive op → dependency bulkhead12headers: Retry-After, remaining, reset1314SEARCH PIPELINE15OLTP commit → CDC/queue → analyze → inverted segments → serve16query: tokenize → postings → BM25 → optional LTR → hydrate SoT17freshness SLO e.g. 30s searchable; deletes filtered on hydrate sooner1819TYPEAHEAD2030k QPS peak class → in-memory trie/FST + hot prefix cache21hot prefix 'a' replicated; nightly rebuild + incremental counts22p99 budget <100ms: few ms lookup + rank + network2324REPORTED ANCHORS25Stripe rate limiters; Cloudflare counting at edge;26Slack hybrid search; Lucene/ES segment mental model2728COMPOSE WITH OTHER DESIGNS29feed/chat/marketplace all need limits on expensive reads30search never replaces SoT; never LIKE% primary at scaleCan you whiteboard a Redis-backed token bucket and an async inverted-index search path with freshness caveats?
Takeaways
- Token bucket = sustained + burst; fixed window is simpler but boundary-bursty.
- Distributed limits: atomic store / sharded owners — not global locks.
- Enforce at multiple layers with different goals.
- Search is a derived inverted index; freshness is an SLO.
- Typeahead is in-memory prefix structures + hot-prefix cache under 100ms p99.
Next: capstone marketplace/dispatch design + mock rubric — and when to switch to the ML/LLM system design companion track.
Sources
Free to read · better with Enzo
Learn it with Enzo
Save your progress, answer the checkpoints, and let Enzo quiz you on what you just read.