Autocomplete/Search Component
System design for search with suggestions, debouncing, race conditions, and caching
Design an autocomplete UI component that allows users to enter a search term, see suggestions in a popup, and select a result.
Real-world examples: Google search bar, Facebook search (rich results), X search
R - Requirements
Key Questions:
- What kind of results? (text, image, media - image + text)
- What devices? (laptop, tablets, mobile)
- Fuzzy search needed? (usually not for initial version)
- Number of results to show? (5, 10, 20)
- Minimum query length? (usually 3+ characters)
Common Answers:
- Text, image, or media results
- All devices (responsive)
- No fuzzy search initially
- 5-10 results typically
A - Architecture
High-Level Flow:
Component Responsibilities:
- Input field UI - Handles user input, passes to controller
- Results UI (Popup) - Receives results, displays, handles selection
- Controller - Brain of component, coordinates all interactions, debounces, manages cache
- Cache - Stores previous queries to avoid server requests (normalized object for O(1) lookup)
Data Flow:
- Client → Server: Search query (debounced, min 3 chars)
- Server → Client: Search results array
- Client → Cache: Query → Results mapping (TTL-based expiration)
Relevant Content:
- Request Deduplication - Avoid duplicate requests
- Data Fetching Strategies - Fetching patterns
D - Data Model
Controller State:
- Props/options (component API)
- Current search string
- Loading/error states
Cache Structure:
- Normalized store (object keyed by query, not array) for O(1) lookup
- Query → Results mapping
- Cache duration/TTL
Implementation Example - Normalized Cache:
// ❌ Array-based cache (slow lookup)
const cache: Array<{ query: string; results: Result[] }> = [];
// O(n) lookup - slow for many queries
// ✅ Object-based cache (fast lookup)
const cache: Record<string, { results: Result[]; timestamp: number }> = {};
// O(1) lookup - fast even with many queries
function getCachedResults(query: string): Result[] | null {
const cached = cache[query];
if (!cached) return null;
// Check TTL (e.g., 5 minutes)
const isExpired = Date.now() - cached.timestamp > 5 * 60 * 1000;
return isExpired ? null : cached.results;
}Race Condition Handling:
- Save results in object/map keyed by search query string
- Only display results corresponding to current input value
- Discard responses from outdated queries
Implementation Example - Race Condition:
let currentQuery = '';
async function search(query: string) {
currentQuery = query; // Track current query
// Check cache first
const cached = getCachedResults(query);
if (cached) {
displayResults(cached);
return;
}
// Fetch from server
const results = await fetchResults(query);
// Only display if this is still the current query
if (currentQuery === query) {
cache[query] = { results, timestamp: Date.now() };
displayResults(results);
}
// If query changed, discard these results
}Relevant Content:
- Caching Patterns - Cache strategies
- Request Deduplication - Race conditions
I - Interface
API Design:
Basic API:
- Number of results
- API URL
- Event listeners (input, focus, blur, change, select)
- Customized rendering (theming, classnames, render function)
Advanced API:
- Minimum query length (3+ characters)
- Debounce duration (300ms typical)
- API timeout duration
- Cache options (initial results, source, merge function, duration)
UX Features:
- Autofocus (if search page)
- Loading spinner
- Error states (with retry)
- No network handling
- Long string truncation (ellipsis)
- Mobile-friendly (large tap targets, autocapitalize/autocomplete/autocorrect="off")
- Global shortcut key (e.g., "/" like Facebook, X, YouTube)
- Query results positioning (above input if no space below)
Accessibility:
- ARIA combobox pattern
aria-label,aria-expanded,aria-haspopuparia-autocomplete="list"(Facebook, X) or"both"(Google)aria-liveregion for results- Keyboard navigation (Enter, Up/Down arrows, Escape)
Relevant Content:
- Accessible Forms - Form patterns
- Keyboard Shortcuts - Keyboard navigation
- ARIA Live Regions - Screen reader announcements
O - Optimizations
Network:
- Debouncing (300ms) - Don't search on every keystroke
- Race condition handling - Query-keyed cache (better than timestamps)
- Request deduplication - Same query = same result
- Cache strategy - Normalized object for O(1) lookup
Performance:
- Virtualized lists - For hundreds/thousands of results
- Result caching - Avoid duplicate requests
- Prefetch popular searches - Load before user types
UX:
- Fuzzy search - Handle typos (Levenshtein distance, server-side)
- Progressive enhancement - Works without JS
Relevant Content:
- Virtualized List - List virtualization
- Interaction to Next Paint - INP optimization
- Prefetching Strategies - Preload popular searches
Implementation Checklist
- MVC architecture (Input UI, Results UI, Controller, Cache)
- Debounce search (300ms)
- Minimum query length (3+ characters)
- Race condition handling (query-keyed cache)
- Request deduplication
- Result caching (normalized object)
- Virtualized list (if many results)
- Keyboard navigation (Enter, arrows, Escape)
- Accessibility (ARIA combobox, screen readers)
- Mobile-friendly (tap targets, input attributes)
- Error handling (timeout, retry, no network)
- Global shortcut key ("/")
Common Pitfalls
❌ Search on every keystroke → Too many requests
✅ Debounce (300ms) to reduce requests
❌ Race conditions → Show wrong results
✅ Query-keyed cache, only show results for current input
❌ No request deduplication → Same query = multiple requests
✅ Cache requests, reuse results
❌ Poor accessibility → Not usable with keyboard/screen readers
✅ ARIA combobox pattern, keyboard navigation, aria-live
Real-World Comparison
Google: aria-autocomplete="both", uses <textarea>, within <form>
Facebook/X: aria-autocomplete="list", uses <input>, no form wrapper
Key Takeaway: No standardized ARIA practice - choose based on use case.