Interview Setup
Interview Prompt
Design a proximity server like Yelp to search 200M businesses within a radius, plus a Nearby Friends variant with real time location sharing for 10M active users.
Clarifying Questions (ask before designing)
| Question | Why it matters |
|---|---|
| Static business search vs real time friend locations: should they share the same index or use separate paths? | Businesses update roughly 1M times per day with low write frequency, whereas friends update every 4 seconds producing 2.5M writes per second. Mixing them in one index creates massive write amplification on the read intensive business path. |
| Between GeoHash, QuadTree, and Redis GEO, which spatial index best fits 200M businesses? | 200M business metadata records total about 200 GB, but the geospatial index stores compact coordinates and business IDs rather than duplicating full records on every search node. GeoHash provides simple compact cell routing, while QuadTree adapts better to uneven density. Redis GEO is reserved for the much more dynamic Nearby Friends workload. |
| How should results be ranked: pure distance, or a combination of distance, rating, and relevance? | A 5-star restaurant 500m away should rank ahead of a 2-star restaurant 100m away. Query execution order matters because filtering by distance first dramatically reduces candidates before evaluating category filters and text relevance. |
| How do reviews affect search without a separate review database? | At roughly 100K reviews per day, denormalizing the average rating directly onto the business record avoids costly table joins at query time while keeping write throughput minimal. |
Scope
In scope
- GeoHash vs QuadTree vs S2 vs H3
- Spatial indexing in Redis
- Radius search
- Ranking by distance + relevance
- Geo-sharding
- Capacity estimation with shown math
Out of scope (state explicitly)
- Full payment processing
- Turn by turn map rendering and navigation
- Driver and rider identity verification and background checks
Functional Requirements
Start by asking your interviewer whether the scope covers static places, moving friends, or both. The update frequency of entities and default search radius dictate the underlying geospatial data structures.
In Scope:
- Search nearby places: Given a user location and radius, return nearby businesses and points of interest such as restaurants, cafes, and ATMs.
- Filter by category: Filter results by business type (restaurant, hospital, ATM, etc.).
- Optional text query: Narrow results by business name or other searchable listing text when a query is supplied.
- Sort and rank: Rank results by distance, average rating, popularity, and relevance.
- Business details: Return comprehensive profiles including name, address, operating hours, photos, and reviews.
- Add or update business: Allow business owners to register listings and update operating details.
- Reviews and ratings: Support user reviews and ratings with real time score aggregations.
- Nearby Friends variant: Stream real time location pings and display active friends within proximity.
Out of Scope:
- Full payment processing (see Payment Gateway).
- Turn by turn map rendering and route navigation (see Map Rendering and Navigation).
- Driver and rider identity verification, criminal background checks, and vehicle compliance.
Non-Functional Requirements
Spatial query latency and write throughput represent the primary evaluation vectors. The read intensive workload justifies aggressive geospatial indexing and cell-based caching.
- Low Latency: Nearby search results returned in under 100 ms.
- High Availability: 99.99% uptime for discovery queries.
- Read to Write Ratio: Approximately 1,000:1 to 10,000:1. Under the stated 100K search QPS and 1M business updates/day assumptions, the workload is about 8,600:1.
- Scalability: Scale across 200M+ businesses and 500M+ daily active users.
- Geospatial Efficiency: Execute bounded spatial queries over massive distributed datasets.
- Real Time Updates: Nearby Friends location updates visible to friends within 5 seconds.
Capacity Estimations
Evaluate capacity figures before sharding the geospatial index. Object count, write velocity, and query QPS dictate whether grid partitioning or an alternative dynamic QuadTree structure is appropriate.
| Metric | Calculation | Value |
|---|---|---|
| Total businesses | Given | 200M |
| Business metadata size | Given (media stored in S3) | 1 KB |
| Total business metadata | 200M x 1 KB | 200 GB |
| Search QPS | Given | 100K |
| Business updates / day | Given | 1M (low frequency) |
| Business updates / sec | 1M ÷ 86,400 | ~11.57/sec |
| Implied search read/write ratio | 100K ÷ 11.57 | ~8,640:1 |
| Nearby Friends: Active users streaming location | Given | 10M |
| Location updates / sec | 10M users ÷ 4s | 2.5M (10M users x 1 update per 4s) |
Architecture Diagram
In the room: clarify nearby places vs nearby friends upfront, because friends require real time location writes whereas place listings are predominantly read-only.
Walk your interviewer through the diagram by separating read from write paths. Static businesses use a production GeoHash index on dedicated search nodes, partitioned by geographic cells. Nearby Friends location updates arrive through WebSocket and overwrite the active coordinate entry. Radius queries inspect only cells that cover the search area. Candidates then undergo exact distance filtering and ranking.
Component Deep Dives
Index structures and update paths are tightly coupled, so understanding both is critical before analyzing query serving.
Business search is read intensive with infrequent writes, whereas friend location is write intensive with frequent reads. Each mode requires a distinct spatial index and caching strategy. The subsections below evaluate GeoHash, QuadTree, Google S2, and Uber H3 approaches, followed by the radius search algorithm and streaming ingestion.
Core Algorithm: Geospatial Indexing
Spatial indexing enables sub 100 millisecond proximity queries across hundreds of millions of coordinates. The production path uses GeoHash, while QuadTree remains an alternative for highly uneven density.
Option 1: GeoHash
Divide the world into 2 halves alternately by longitude and latitude, then encode the binary coordinates as Base32 strings. Nearby locations share a common GeoHash prefix.
GeoHash Precision: Length 1: ~5,000 km cell Length 4: ~39 km cell Length 5: ~4.9 km cell (optimal for nearby search) Length 6: ~1.2 km cell (optimal for walking distance) Length 7: ~150 m cell Length 8: ~38 m cell Key property: Nearby locations share a common GeoHash prefix. "9q8yy" and "9q8yz" are adjacent cells. Search algorithm: 1. Compute the GeoHash precision from the requested radius 2. Determine the covering cell set for the search circle 3. For the common 5 km search at precision 5, query the home cell plus its 8 neighbors: WHERE geohash LIKE '9q8yy%' 4. For larger radii, expand the covering set beyond 9 cells as needed 5. Filter by actual distance because GeoHash cells are rectangular approximations 6. Sort by the requested ranking mode
Option 2: QuadTree (alternative in memory structure)
Start with the entire world bounding box as the root node. Recursively divide each node into 4 quadrants (NW, NE, SW, SE) until a node contains at most K businesses (for example, K=100). Leaf nodes contain the actual business records.
Root (World) ├── NW (Americas) │ ├── NW (Canada) │ ├── NE (NE US) -> [biz1, biz2, ..., biz87] (leaf: 87 businesses) │ ├── SW (SW US) │ │ ├── NW (Bay Area) -> [biz1, ..., biz95] (leaf) │ │ ├── NE (Central CA) │ │ └── ... │ └── SE (SE US) ├── NE (Europe/Asia) └── ... Search algorithm: 1. Start at the root and recursively traverse nodes whose bounding boxes intersect the search circle 2. Prune nodes whose bounding boxes are entirely outside the search radius 3. Collect business candidates from intersecting leaf nodes 4. Continue until all intersecting nodes are examined or the bounded candidate set is satisfied 5. Filter by exact distance Building QuadTree: O(N log N), where N = number of businesses Search: O(log N + M), where M = businesses in result Memory: 200M businesses x 32-byte compact entries = ~6.4 GB (fits in memory on a single server) Rebuild: Every night. Incremental updates during the day
Option 3: Google S2 Geometry
- Uses a space-filling Hilbert curve to map two-dimensional surface space to a one-dimensional sequence
- Encodes each cell into a compact 64-bit integer identifier
- Provides minimal cell covering set logic for arbitrary shapes and polygons
- Maintains more uniform cell sizes than GeoHash across varying latitudes
- Commonly used in production by Google Maps and Foursquare
Option 4: Uber H3 Index
- Hexagonal grid system rather than square or rectangular cells
- Hexagons have uniform distance to all 6 immediate neighbors, unlike squares where diagonal neighbors are farther
- Resolution 9 (~174m edge) for precise tracking, and Resolution 7 (~1.2km edge) for surge pricing zones
- Finding adjacent neighbors is an efficient O(1) mathematical operation
Proximity Search Service
Radius queries hit overlapping grid cells, then filter candidates and rank them along the critical read path.
- Receive query:
{lat, lng, radius, category, sort_by} - Query the production GeoHash index for businesses within the covering cells. QuadTree remains an alternative implementation for regions with highly uneven density.
- Filter candidates by business category code and optional text query
- Rank candidates by the requested sort mode. Distance uses exact distance, while relevance can combine distance, average rating, popularity, and optional text relevance.
- Enrich top candidates with business details from Redis cache or fall back to MySQL
- Return paginated results with a cursor containing the last ranking key and business_id for stable page boundaries
Business Service
Business listings are predominantly static, which allows indexing once and updating asynchronously on owner edits.
- Handles CRUD operations for business listings
- On create or update, write changes to MySQL. Debezium CDC then emits the asynchronous change event used to update the geospatial index.
- Review create or update writes the review row and adjusts
rating_sum,review_count, andavg_ratingin one transaction. The resulting business row change is captured by the same CDC pipeline used for index refresh. - On delete, mark the business inactive and let CDC remove it from the GeoHash index and invalidate its Redis profile cache.
- Hot business profiles remain cached in Redis to minimize relational database read load
Kafka CDC: Incremental GeoHash Updates
Change data capture decouples database writes from in memory index updates and ensures consistency across search replicas.
- A full GeoHash index rebuild from MySQL executes nightly, taking roughly 5 minutes for 200M businesses. The rebuild records a consistent snapshot and CDC position, builds a replacement index from that snapshot, replays subsequent CDC events before cutover, and swaps the active index only after the new version catches up so no updates are lost during the rebuild.
- Debezium CDC on MySQL captures row-level changes and publishes them to the Kafka topic
business-changes - The GeoHash index cluster consumes changes:
- Insert: Compute the business's GeoHash cell and add its compact index entry.
- Update location: Remove the business from its old cell and insert it into the new cell.
- Update metadata: Refresh compact searchable fields without changing the cell when location is unchanged.
- Delete: Remove the business entry from its GeoHash cell.
- The CDC stream also invalidates the Redis business cache and updates Redis GeoIndex
- Typical end-to-end lag: catalog updates reflect in search results within approximately 2 seconds, with a < 30 second freshness SLO
Nearby Friends: Real Time Architecture
Nearby friends requires high throughput real time writes, with privacy enforced by limiting what location information is exposed to other users.
- Unlike static business search, moving friends produce high frequency location streams that require an in memory spatial store.
- Execution workflow:
- Each user streams ephemeral location coordinates every 4 seconds over a WebSocket connection
- Location writes update a city and GeoHash prefix scoped Redis Geo Set. A 15 second expiry index removes stale members, and disconnect handlers remove entries when possible
- When a user opens Nearby Friends, the service queries nearby candidates and intersects them with the user's friend set
- Optimization: Use the spatial index to pre-filter nearby candidates before friend-set membership checks
Reviews and Rating Denormalization
Denormalize ratings onto business records so proximity search queries never require expensive table joins.
At roughly 100K reviews per day (~1.16 writes/sec), reviews stay in the same MySQL database as businesses, so no separate review service or message queue is required. Each review insert or update runs in one transaction. A new review adds its rating to rating_sum and increments review_count. An edited review applies the difference between the old and new rating. The transaction then recomputes avg_rating = rating_sum / review_count. A UNIQUE constraint on (user_id, business_id) enforces one review per user per business. Search reads the denormalized avg_rating directly from Redis cache or the GeoHash index without joining the review table.
Event Bus Design (Kafka)
The Kafka event bus decouples relational write streams from in memory index consumers and buffers peak ingestion bursts.
Topic: business-changes (Debezium CDC from MySQL)
Partitions: 16 (partition by business_id)
Partition key: business_id
Retention: 24 hours (index catch-up only)
Replication factor: 3, min.insync.replicas: 2
Producer: Debezium connector on MySQL binlog (INSERT/UPDATE/DELETE on businesses table)
Consumer: GeoHash index updater (consumer group "proximity-index-sync")
- Incremental cell update: remove old GeoHash cell, insert into new cell
- Invalidate Redis cache key biz:{business_id}
- At ~12 events/sec (1M updates/day), single consumer suffices
- Lag alert: CDC lag > 30s indicates search index staleness
Sync path: business owner updates listing, writes to MySQL, and receives 200 OK
Async path: CDC event updates the in memory GeoHash index and invalidates the Redis cache. Typical propagation is around 2 seconds, with a <30 second freshness SLO.API Design
Search Nearby
Nearby search is the primary read operation. Business management, reviews, and friend location use the endpoints below.
GET /api/v1/search/nearby?lat=37.7749&lng=-122.4194&radius=5000&category=restaurant&q=pizza&sort=distance&limit=20&cursor=...
Response: 200 OK
{
"businesses": [
{
"business_id": 123456,
"name": "Joe's Pizza",
"category": "restaurant",
"address": "123 Main St, San Francisco, CA",
"lat": 37.7751,
"lng": -122.4190,
"distance_meters": 45,
"rating": 4.5,
"review_count": 234,
"price_level": "$$",
"is_open": true,
"photo_url": "..."
}
],
"has_more": true,
"next_cursor": "eyJsYXN0X3NvcnRfa2V5IjoiLi4uIiwiaWQiOjIwMDB9"
}Get Business Details
GET /api/v1/businesses/{business_id}Add or Update Business
POST /api/v1/businesses
{ "name": "Joe's Pizza", "lat": 37.7751, "lng": -122.4190, ... }Create Review
POST /api/v1/businesses/{business_id}/reviews
{ "rating": 5, "body": "Great pizza!" }List Reviews
GET /api/v1/businesses/{business_id}/reviews?limit=20&cursor=...Nearby Friends (Real Time Variant)
WebSocket /ws/v1/nearby-friends
{
"type": "location_update",
"lat": 37.7749,
"lng": -122.4194
}
Server Push:
{
"type": "nearby_friends",
"friends": [
{"user_id": "...", "name": "Alice", "distance_meters": 120, "last_updated": "10s ago"}
]
}Common Error Responses
400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff
440 Login Timeout: WebSocket connection session expired, client reconnect is required
504 Gateway Timeout: search index shard responded slowly, narrow query parameters or retryData Model
MySQL: Business Listings
The production geospatial index stores compact business entries keyed by GeoHash cell. Redis also serves hot business profiles and real time nearby locations.
CREATE TABLE businesses (
business_id BIGINT PRIMARY KEY,
name VARCHAR(256),
category VARCHAR(64),
address TEXT,
city VARCHAR(128),
state VARCHAR(64),
country VARCHAR(64),
lat DECIMAL(10,7),
lng DECIMAL(10,7),
geohash VARCHAR(12),
phone VARCHAR(20),
website TEXT,
price_level TINYINT,
avg_rating DECIMAL(2,1) DEFAULT 0.0,
rating_sum BIGINT DEFAULT 0,
review_count INT DEFAULT 0,
hours JSON,
photos JSON,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
INDEX idx_geohash (geohash),
INDEX idx_category_geohash (category, geohash),
INDEX idx_city (city)
);MySQL: Reviews
CREATE TABLE reviews (
review_id BIGINT PRIMARY KEY,
business_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
rating TINYINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
body TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE KEY uq_user_business (user_id, business_id),
INDEX idx_business_created (business_id, created_at)
);
-- Reviews are co-located with the business_id shard.In Memory QuadTree Node
interface Business {
businessId: number;
lat: number;
lng: number;
category: string;
avgRating: number;
}
interface QuadTreeNode {
// Bounding box coordinates
x1: number;
y1: number;
x2: number;
y2: number;
// Non-null only for leaf nodes (max 100 businesses per leaf)
businesses?: Business[];
// Child quadrants (null or undefined for leaf nodes)
nw?: QuadTreeNode;
ne?: QuadTreeNode;
sw?: QuadTreeNode;
se?: QuadTreeNode;
}Redis: Business Cache & GeoIndex
# Business details cache
Key: biz:{business_id}
Value: Hash { name, category, lat, lng, avg_rating, review_count, ... }
TTL: 3600
# Redis GeoIndex (alternative to the in memory GeoHash index)
GEOADD businesses:restaurant {lng} {lat} {business_id}
GEOSEARCH businesses:restaurant FROMLONLAT {lng} {lat} BYRADIUS 5 km
WITHDIST COUNT 20 ASCRedis: Nearby Friends (Real Time)
# Current location index, sharded by city and GeoHash prefix
# Keep the last location shard so movement can remove the stale entry.
# Perform the shard move and metadata update atomically (for example with a Lua script)
# If city or geohash_prefix changed, remove the old entry first:
ZREM geo:{old_city}:{old_geohash_prefix} {user_id}
ZREM geo_expiry:{old_city}:{old_geohash_prefix} {user_id}
GEOADD geo:{city}:{geohash_prefix} {lng} {lat} {user_id}
ZADD geo_expiry:{city}:{geohash_prefix} {expires_at_epoch} {user_id}
HSET user_location_meta:{user_id} city {city} geohash_prefix {geohash_prefix}
# Query all GeoHash shards covering the search circle
GEOSEARCH geo:{city}:{geohash_prefix} FROMLONLAT {lng} {lat} BYRADIUS 5 km
WITHDIST COUNT 500 ASC
# Expand or paginate the candidate set if needed
# Expiry sweeper removes stale members from both indexes
ZRANGEBYSCORE geo_expiry:{city}:{geohash_prefix} -inf {now_epoch}
ZREM geo:{city}:{geohash_prefix} {expired_user_id}
ZREM geo_expiry:{city}:{geohash_prefix} {expired_user_id}
# Intersect the bounded candidate set with the user's friend set
for candidate_id in candidates:
if SISMEMBER friends:{user_id} candidate_id:
add to resultKafka Topics
Topic: business-changes (Debezium CDC from MySQL)
Partition key: business_id
Consumers: GeoHash index updater, Redis biz:{id} cache invalidationFault Tolerance
Mitigate index lag on fast-moving objects, boundary effects at cell edges, and hot-spotting in dense urban centers.
| Concern | Solution |
|---|---|
| GeoHash index server failure | Multiple replicas, with full GeoHash index rebuild from MySQL completing in under 5 minutes |
| MySQL read failure | Read replicas distributed across multiple availability zones |
| Stale geospatial index | Incremental updates during peak hours paired with a full nightly rebuild |
| Cache miss storm | Request coalescing combined with stale while revalidate caching |
GeoHash and Cell Boundary Mitigation
Boundary conditions require specific neighbor querying to prevent false negative omissions.
- A user near the edge of a grid cell risks missing businesses located just across the cell boundary.
- Neighbor expansion: For the common 5 km search at precision 5, query the home cell plus its 8 neighbors. Larger radii use the full covering cell set required by the search circle.
- QuadTree traversal: Expand boundary searches into sibling quadrant nodes whenever candidate counts near the perimeter are insufficient.
Additional Considerations
Scaling the In-Memory Spatial Index
Privacy controls with fuzzed locations, dynamic geofencing, and historical location trails are natural follow-up topics.
- 200M businesses x 32 bytes ≈ 6.4 GB for compact spatial index entries, which fits in memory on a single commodity server.
- Replicate the in memory GeoHash index across multiple search servers to satisfy 100K QPS.
- For global scale, shard the GeoHash index by geographic regions such as North America, Europe, and Asia.
Density-Based Search Radius
- In dense urban cores like Manhattan, configure the default search radius to 500 meters.
- In rural areas with sparse listings, expand the default search radius to 50 kilometers.
- Adaptive radius: begin with a localized radius and expand progressively until at least 10 results are retrieved.
Business Hours and Open/Closed Status
- Store operating hours per day in JSON format:
{"mon": {"open": "09:00", "close": "22:00"}, ...} - During search execution, filter by "open now" against the user's local timezone, which is derived from query coordinates via a timezone polygon lookup table.
Architectural Context and Related Systems
Proximity indexing forms the geospatial foundation for multiple real time and discovery platforms. Dynamic driver-rider matching with moving entities is explored in Ride Hailing (Uber), while dispatch and delivery route estimation are detailed in Food Delivery. Vector tile rendering and shortest-path routing omitted from this scope are covered in Map Rendering and Navigation, and checkout billing pipelines integrate with Payment Gateway. Real-time friend status and connection lifecycle management connect to User Presence System. For foundational concepts, review Consistent Hashing, Caching Patterns and Invalidation, Redis Patterns for Interview Systems, Sharding and Partitioning, Back-of-the-Envelope Estimation, and System Design Interview Patterns.
Interview Walkthrough
- 25 minute cut
Skip arch50 and arch75 depth unless interviewing for a staff-level role.
- Clarify functional requirements and scope separation between places and friends (3 min)
- Geospatial grid indexing and candidate retrieval for radius queries (8 min)
- WebSocket location update ingestion and Redis GEO storage (6 min)
- User location privacy via coordinate quantization (4 min)
- Hot-cell caching and request coalescing for dense urban areas (4 min)
- Scope first: Differentiate between the two architectural modes immediately. Static business search manages 200M points of interest with low write frequency, whereas dynamic Nearby Friends tracks 10M moving users. Maintain separate storage and indexing paths so high velocity location writes never degrade business search latency.
- Frame the core challenge as a geospatial nearest neighbor problem bounded by a strict 100 ms latency service level objective. Establish the spatial indexing structure early by comparing GeoHash grid bucketing with QuadTree partitioning, then select GeoHash for the stated workload.
- Detail how client devices stream ephemeral location pings over persistent WebSocket connections to stateful gateways, which buffer writes and update regional Redis Geo sets sharded by geohash prefix, applying concepts from Redis Patterns for Interview Systems.
- Address location privacy directly by keeping precise coordinates within the trusted backend and exposing only relative distances or an approximate zone to other users.
- Discuss Caching Patterns and Invalidation to handle hot urban cells where search density threatens to saturate individual index shards, employing request coalescing and stale while revalidate headers.
- Enforce a short time to live on active location records, around 15 seconds for a 4 second update interval, and remove the entry on disconnect when possible. This prevents stale records from creating phantom nearby users.
- Avoid the common pitfall of computing Haversine distance across all pairs because quadratic comparison collapses at scale. Always query spatial index cells first to narrow candidates, then apply precise Haversine distance filtering only on candidate matches.
Engineering Trade-offs
GeoHash vs QuadTree vs Redis Geo Set vs S2 vs H3
Spatial index selection represents the central trade-off of this system. Compare grid indexing with QuadTree hierarchies and justify the production GeoHash choice for nearby place search.
Approach Query Type Scale Pros Cons Best For
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
GeoHash index Radius 200M+ Simple, compact, prefix-friendly Cell boundary edge cases, covering cells Large static catalogs with regional sharding
search
QuadTree (in-mem) Radius + arb 200M+ Fast, <6.4 GB for 200M biz, flexible Requires dedicated servers, rebuild Yelp-scale static data
query overhead
Redis Geo Set Radius only 100M Zero infrastructure, built-in No polygon queries, precision degrades Simple proximity (Tinder, Uber)
GEORADIUS near poles
Google S2 Polygon/rad Unlimited Most accurate, hierarchical, Application-level library, no built-in DB Google Maps, complex geo queries
Hilbert curve
PostGIS Any 100M Full spatial SQL, arbitrary shapes Harder to scale horizontally Complex shapes, GIS appsRule of thumb: For static points of interest and businesses, prefer QuadTree or GeoHash indexes. For high frequency moving entities such as users, riders, or vehicles, choose Redis Geo sets.
Grid Search vs Pure Radius Trade-off
Two common implementations for "find businesses within 5 km":
Approach 1: Cell-based (GeoHash / QuadTree)
Find the cell(s) covering the search area, then return all businesses in those cells
Pros: Fast index lookup
Cons: Returns businesses in rectangular area, not a circle
Must post-filter to calculate actual distance for each result
Produces more candidates than needed because rectangle corners extend beyond radius
Optimization: use tighter cells (resolution 6: ~1.2 km)
More cells to query (25 cells for 5 km radius at res 6 vs 9 at res 5)
Fewer false positives, resulting in less post-filtering
Approach 2: Sorted distance (brute force in memory)
Load all nearby businesses, compute exact Haversine distance, sort
Only works if "nearby" is well-bounded (< 100K candidates)
For dense cities: 100K businesses in 5 km results in slow scans
For sparse areas: 50 businesses in 50 km is trivial
Best practice:
1. Query grid cells to retrieve candidate set (~1K businesses)
2. Execute exact Haversine distance calculation to filter to actual radius
3. Sort by distance and paginate without returning all 1K candidates
Flat-earth approximation (fast, no trig, accurate to 0.1% for < 50 km):
d = sqrt((Δlat)² + (Δlng x cos(lat))²) x 111 km/degreeNearby Friends: Location Privacy and Precision Levels
Privacy concern: sharing exact GPS coordinates exposes home/work locations. Solution: Use precision-reduced location sharing Level 1: Exact coordinates (high privacy risk): Share full GPS coordinates: lat=37.774929, lng=-122.419418 Distance accurate to meters, revealing the exact building AVOID for social features Level 2: Fuzzy location (recommended for Nearby Friends) ⭐: Truncate coordinates to 3 decimal places: lat=37.774, lng=-122.419 Precision: roughly 100 to 150 meters of grid uncertainty "Alice is about 500m away", which prevents pinpointing the exact home Still accurate enough for "Alice is in this neighborhood" Level 3: GeoHash cell (for approximate zone): Share GeoHash of precision 5 (4.9 km x 4.9 km cells) "Alice is in this part of downtown", identifying an approximate neighborhood rather than a precise location Works for: "Friends in the same neighborhood" features Implementation: User sends precise GPS to the trusted backend over TLS only when location sharing is enabled Backend stores precise location only in regional Redis for proximity computation. A 15 second expiry index removes stale members, and disconnect handlers remove entries when possible API response: returns ONLY distance in meters or an approximate zone, never raw coordinates Frontend: shows "Alice is 800m away", preventing direct exposure of the exact location Privacy toggle: user can set "share location with friends" ON/OFF When OFF: friends see "Alice is in San Francisco" (city-level only)
Review
How helpful was this walkthrough?
Click a star to rate. We actively use this feedback to refine and update our system design content.
Discussion
Share your thoughts, ask questions, or help others.