System Design Problem

Design a Proximity Server (Yelp / Nearby Friends)

Commonly Asked By:UberLyftGoogleMetaAirbnb

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)

QuestionWhy 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.

MetricCalculationValue
Total businessesGiven200M
Business metadata sizeGiven (media stored in S3)1 KB
Total business metadata200M x 1 KB200 GB
Search QPSGiven100K
Business updates / dayGiven1M (low frequency)
Business updates / sec1M ÷ 86,400~11.57/sec
Implied search read/write ratio100K ÷ 11.57~8,640:1
Nearby Friends: Active users streaming locationGiven10M
Location updates / sec10M users ÷ 4s2.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.

Loading...

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.

  1. Receive query: {lat, lng, radius, category, sort_by}
  2. Query the production GeoHash index for businesses within the covering cells. QuadTree remains an alternative implementation for regions with highly uneven density.
  3. Filter candidates by business category code and optional text query
  4. Rank candidates by the requested sort mode. Distance uses exact distance, while relevance can combine distance, average rating, popularity, and optional text relevance.
  5. Enrich top candidates with business details from Redis cache or fall back to MySQL
  6. 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, and avg_rating in 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.

YAML
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.

HTTP
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

HTTP
GET /api/v1/businesses/{business_id}

Add or Update Business

HTTP
POST /api/v1/businesses
{ "name": "Joe's Pizza", "lat": 37.7751, "lng": -122.4190, ... }

Create Review

HTTP
POST /api/v1/businesses/{business_id}/reviews
{ "rating": 5, "body": "Great pizza!" }

List Reviews

HTTP
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

JSON
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 retry

Data 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.

SQL
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

SQL
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

TYPESCRIPT
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

REDIS
# 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 ASC

Redis: Nearby Friends (Real Time)

REDIS
# 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 result

Kafka Topics

YAML
Topic: business-changes (Debezium CDC from MySQL)
  Partition key: business_id
  Consumers: GeoHash index updater, Redis biz:{id} cache invalidation

Fault Tolerance

Mitigate index lag on fast-moving objects, boundary effects at cell edges, and hot-spotting in dense urban centers.

ConcernSolution
GeoHash index server failureMultiple replicas, with full GeoHash index rebuild from MySQL completing in under 5 minutes
MySQL read failureRead replicas distributed across multiple availability zones
Stale geospatial indexIncremental updates during peak hours paired with a full nightly rebuild
Cache miss stormRequest 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 apps

Rule 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/degree

Nearby 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

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...