Design a Proximity Service (Yelp / Places Nearby)
1. Problem Statement & Scope Clarification
System Mission
Design a planetary-scale, highly available, low-latency proximity discovery platform (equivalent to Yelp, Google Places, or Foursquare) capable of indexing hundreds of millions of static Points of Interest (POIs), restaurants, and commercial venues. The system must ingest user geographic coordinates (latitude, longitude) and a search radius (), evaluate dynamic spatial filters (categories, price tiers, minimum star ratings, operational hours), and return the -nearest venues with sub-30 millisecond P99 latency while absorbing over 125,000 peak search queries per second.
At a glance:
| Metric | Value |
|---|---|
| POI Scale | 200M Registered Venues |
| Read Peak | 125,000 QPS |
| Read/Write Ratio | ≈ 750 : 1 (Section 2) |
| P99 Search Latency | < 30ms |
| Availability SLA | 99.999% |
| Precision | 100% Geodesic WGS84 |
| Primary DB | Aurora PostGIS (GiST) |
| Spatial Cache | ElastiCache Redis |
| Search | Amazon OpenSearch |
Functional Requirements
- Search Nearby POIs (
SearchNearby): Given a user's latitude, longitude, and radius (e.g., , , , , ), return a paginated list of matching businesses sorted by straight-line geodesic distance or algorithmic relevance. - Multi-Attribute Filtering & Ranking: Allow users to filter results by category (e.g., "coffee", "italian"), price tier (
$to$$$$), open status (is_open_now), and minimum rating (). - POI Profile Management (
CreateOrUpdatePlace): Enable business owners to create and update venue profiles, operational hours, photos, and address coordinates with eventual consistency (propagation delay ). - Detailed Place View (
GetPlaceDetails): Retrieve complete business profile data, photo galleries, operational schedules, and aggregated customer reviews. - Dynamic Precision Scaling: Automatically adapt spatial search resolution to balance density skew between dense urban centers (e.g., Manhattan, Tokyo) and sparse rural zones (e.g., rural Montana, Outback).
Non-Functional Requirements (SLAs & SLOs)
- Ultra-Low Latency:
- Nearby Search Path: , for cached/indexed spatial lookups.
- POI Details Path: from edge CDN / in-memory cache.
- High Availability: uptime SLA ( unscheduled downtime/year). The read discovery path must degrade gracefully during database maintenance or regional failovers.
- Geodesic Precision: Zero boundary-crossing omissions ("fence paradox"). Distance calculations must use WGS-84 ellipsoidal or Haversine spherical math rather than Euclidean approximations.
- Data Durability & Consistency: durability for master business records. Reads are eventually consistent ( replication delay acceptable for newly posted venue updates).
- Scalability: Seamless horizontal scaling to absorb massive lunch and dinner traffic spikes ( diurnal variation).
The Fence Paradox Invariant: A single Geohash prefix query silently drops every venue that sits across a cell edge, however close it is: a user standing 10 m from a cell boundary sees the café 900 m away inside the cell and not the one 15 m away outside it. Production proximity services must unconditionally expand queries to a grid (center cell plus all 8 topological neighbors), and choose a precision whose cell is at least as large as the search radius so the grid actually covers the whole circle, before executing in-memory geodesic Haversine distance filtering.
2. Capacity & Scale Estimation (Back-of-the-Envelope Math)
Traffic Volume & Query Throughput (QPS)
- Daily Active Users (DAU): ().
- Search Queries per User: .
- Total Daily Search Queries:
- Average Search QPS:
- Peak Search QPS ( peak diurnal dinner rush & weekend spikes):
- Write QPS (Business Updates & Registrations):
- total registered businesses.
- Assume each business updates metadata or hours once every 30 days:
- Read-to-Write Ratio:
Storage Footprint & Capacity Growth (5-Year Horizon)
Every business entity comprises structured metadata, location vectors, and review summaries:
- Core Business Profile:
business_id(UUID 16B),name(128B),address(256B),category_id(4B),price_tier(1B),phone(32B),operational_hours_json(256B),rating_avg(4B),review_count(4B) . - Geospatial Coordinates:
latitude(8B float64),longitude(8B float64),geohash_code(12B string),h3_index(8B uint64), PostGISgeography(Point, 4326)(32B) . - Media & Description Metadata: Image URLs, rich tags, amenities bitmask .
- Total Storage per Business Entity: .
- 5-Year Growth ( YoY venue expansion):
- Accounting for B-tree and PostGIS Generalized Search Tree (GiST) indexes () and 3-AZ storage replication:
Network Bandwidth Sizing
- Search Request Payload: User coordinates, radius, filter params .
- Search Response Payload: Top 20 nearby venues with summary cards JSON.
- Peak Ingress Bandwidth:
- Peak Egress Bandwidth:
In-Memory Cache Sizing (ElastiCache Redis)
Using the 80/20 Pareto principle, of geographic areas (dense metropolitan centers) generate of all search traffic.
- Total urban Geohash precision 6 cells globally (each ): .
- Each cell contains an average of 150 business IDs:
- Active Cell Index Working Set:
- Cached Top 20% Business Detail Objects (40M venues):
- Total Redis Working Set (with index overheads): of data, which must fit across the primaries of a cluster-mode deployment. Provision 3 shards of
cache.r6g.2xlarge( each, total, headroom) with one replica per shard; threecache.r6g.xlargenodes ( total) would not hold the working set even before fragmentation.
Compute Fleet Sizing (ECS Fargate)
- Peak Request QPS: .
- A high-performance Go or Rust microservice container (2 vCPU, 4 GB RAM) handles with in-memory Redis queries and connection pooling.
3. AWS-First High-Level Architecture
The architecture partitions responsibilities into four distinct tiers: Edge & Ingress, Spatial Indexing & Caching Tier, PostGIS Spatial Persistence Tier, and Asynchronous Search & Invalidation Pipeline.
Synthesizing vector architecture diagram...
Follow a "places near me" request from the top. After the edge tier, the ALB sends searches to the proximity service and edits to the CRUD service. In the "In-Memory Geospatial Acceleration Tier" panel, the search first uses Redis: the user's geohash cell and its neighbors, or a GEO radius query, return candidate place IDs in about a millisecond. On a cache miss, the "Master Relational Spatial Persistence" panel takes over: RDS Proxy sends reads to Aurora replicas, which answer with PostGIS ST_DWithin over a GiST index, while writes go to the primary. In the "Full-Text Search & Invalidation Pipeline" panel, every place change flows through CDC to a Lambda that clears the affected Redis cells and updates OpenSearch, which answers name and cuisine searches. The write path never touches caches directly; the change log keeps every read copy in sync.
Data Flow Walkthrough
- Perimeter Ingress: The mobile client dispatches
GET /v1/places/nearby?lat=37.7749&lon=-122.4194&radius_m=2000. Route 53 directs the request to the nearest AWS edge point of presence. AWS WAF verifies request token rate limits and forwards it through the ALB to an ECS Fargate Proximity Service container. - Spatial Coordinate Discretization: The Proximity Service picks the coarsest Geohash precision whose smaller cell dimension is still the requested radius, so that the center cell plus its 8 neighbors is guaranteed to contain the entire search circle (Section 8.1). For
radius_m=2000that is precision 5 (, e.g.9q8yy); precision 6 (, e.g.9q8yyk) is used for radii up to . - 8-Neighbor Expansion & Fence Paradox Defense: To eliminate boundary-crossing omissions ("fence paradox"), the service computes the target cell and its 8 adjacent neighboring Geohashes (a cell matrix totaling 9 Geohashes).
- Multi-Key Cache Lookup: The service pipelines nine
SMEMBERScalls across the 9 Geohash tile keys (geo:tile:9q8yy,geo:tile:9q8yz, ...; the tiles are Redis SETs, soMGETcannot read them). If all keys hit, Redis returns sets of candidate venue IDs. - Exact Distance Filtering & Truncation: The service retrieves coordinates for candidates, evaluates the exact geodesic Haversine distance from the user's coordinate, discards any points where , applies multi-attribute filters (e.g.,
rating >= 4.0), sorts ascending by distance, and returns the top 20 venues. - Cache-Miss DB Fallback via RDS Proxy: If any of the 9 Geohashes is absent from Redis, the query falls back through AWS RDS Proxy to an Amazon Aurora PostgreSQL PostGIS Read Replica, executing an index-accelerated
ST_DWithinquery against the GiST spatial index. RDS Proxy multiplexes connections to prevent pool exhaustion during spatial bursts, while routing around replicas withAuroraReplicaLag > 100ms. The retrieved entities are asynchronously backfilled into Redis with a 24-hour jittered TTL. - Write & Invalidation Path: Venue updates (
POST /v1/places) write strictly through RDS Proxy to the Aurora PostgreSQL Primary. PostgreSQL CDC via AWS DMS streams change events to Amazon EventBridge. A lightweight Lambda worker purges or updates the affected Redis Geohash tile and synchronizes the document in Amazon OpenSearch.
Core Request Tracing Execution Walkthrough
| Step # | Event / Action | Component State | Distributed Transition | Output / Response |
|---|---|---|---|---|
| Step 1 | Client initiates nearby search with coordinates and radius | Ingress edge at CloudFront / WAF | Route 53 GeoDNS directs to nearest AWS Region ALB | TLS termination; WAF token validation passed |
| Step 2 | Service computes base Geohash and 8-neighbor bounding cells | In-memory stack on ECS task | Precision 5 chosen for ; spatial math converts float coordinates to 9 Geohash strings | 9 Geohashes generated: [9q8yy, 9q8yz, 9q8yx, 9q8yw, 9q8yt, 9q8yv, 9q8zn, 9q8zj, 9q8zh] (center + N, NE, E, SE, S, SW, W, NW) |
| Step 3 | Service executes pipelined batch lookup against Redis | ElastiCache Redis Cluster | Pipelined SMEMBERS geo:tile:<hash> (one per cell; MGET only works on string keys) across cluster hash slots | Redis returns candidate business ID sets for all 9 cells |
| Step 4 | Partial cache miss detected (1 of 9 cells expired) | Proximity Service cache manager | Circuit breaker verifies RDS Proxy & Aurora replica health | Fallback query dispatched through RDS Proxy pool |
| Step 5 | Aurora executes PostGIS ST_DWithin with GiST index scan | PostGIS Read Replica | Spatial bounding box scan over GEOGRAPHY(Point, 4326) | Aurora returns matching venue rows with exact coordinates |
| Step 6 | Async worker backfills missing Geohash key in Redis | Lambda / Background task pool | SADD geo:tile:<missing_hash> <ids> with EXPIRE 86400 | Redis tile cache warmed for subsequent concurrent queries |
| Step 7 | In-memory Haversine filtering, attribute gating & ranking | Proximity Service memory buffer | Exact geodesic distance computed; non-qualifying points pruned | Sorted array of top 20 venues assembled |
| Step 8 | Final HTTP payload serialization and egress response | Application Load Balancer | JSON serialized with cache headers (Cache-Control: private) | HTTP 200 OK returned to mobile app in |
4. API Interface Design & Data Contracts
1. Search Nearby Places (GET /v1/places/nearby)
Discovers places within a specified radius of a geographic coordinate with optional filtering.
Request Headers & Query Parameters
httpGET /v1/places/nearby?lat=37.774929&lon=-122.419416&radius_m=2000&category=coffee&min_rating=4.0&price_tier=2&limit=20&page_token=eyJvZmZzZXQiOjIwfQ HTTP/1.1 Host: api.places.platform.aws.internal Authorization: Bearer jwt_live_99a8b7c6d5e4f3a2b1 X-Correlation-ID: corr_01J8N6K3P4V9QZ2W8M1Y7R4X Accept: application/json
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
lat | Float64 | Yes | — | Latitude in degrees ( to ) |
lon | Float64 | Yes | — | Longitude in degrees ( to ) |
radius_m | Integer | No | 2000 | Search radius in meters ( to ) |
category | String | No | — | Filter by category slug (e.g., coffee, restaurants) |
min_rating | Float32 | No | 0.0 | Filter by minimum average rating ( to ) |
price_tier | Integer | No | — | Bitmask or tier (1 = $, 2 = $$, 3 = $$$, 4 = $$$$) |
open_now | Boolean | No | false | Whether venue must currently be open based on local time |
limit | Integer | No | 20 | Results per page ( to ) |
page_token | String | No | — | Opaque cursor token for offset-free pagination |
Response: 200 OK
json{ "places": [ { "business_id": "biz_9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "name": "Blue Bottle Coffee - Mint Plaza", "categories": ["coffee", "cafes"], "coordinates": { "latitude": 37.778291, "longitude": -122.418294 }, "distance_meters": 384.2, "rating": 4.6, "review_count": 1842, "price_tier": 2, "is_open_now": true, "primary_photo_url": "https://cdn.platform.aws/photos/biz_9b1d_cover.webp", "address": { "street": "66 Mint St", "city": "San Francisco", "state": "CA", "postal_code": "94103", "country": "US" } } ], "next_page_token": "eyJvZmZzZXQiOjIwfQ==", "search_metadata": { "center": { "latitude": 37.774929, "longitude": -122.419416 }, "radius_meters": 2000, "matched_count": 47, "geohash_precision_used": 5, "latency_ms": 14.8 } }
2. Create or Update Place Profile (POST /v1/places)
Enables authorized merchant admins to register or update venue profiles.
Request Payload
json{ "name": "Sightglass Coffee Roasters", "categories": ["coffee", "bakery"], "coordinates": { "latitude": 37.776912, "longitude": -122.408544 }, "address": { "street": "270 7th St", "city": "San Francisco", "state": "CA", "postal_code": "94103", "country": "US" }, "phone": "+14158611313", "price_tier": 2, "operating_hours": { "monday": [{ "open": "07:00", "close": "17:00" }], "tuesday": [{ "open": "07:00", "close": "17:00" }], "wednesday": [{ "open": "07:00", "close": "17:00" }], "thursday": [{ "open": "07:00", "close": "17:00" }], "friday": [{ "open": "07:00", "close": "18:00" }], "saturday": [{ "open": "07:30", "close": "18:00" }], "sunday": [{ "open": "07:30", "close": "17:00" }] } }
Response: 201 Created
json{ "business_id": "biz_8a2ceb3f-2a1c-4cf8-8b9a-1a0e6c2dcb5e", "status": "ACTIVE", "geohash_code": "9q8yyk3m2p98", "created_at": "2026-09-16T14:32:00Z" }
Status Codes & Error Contracts
200 OK: Successful proximity query or place profile retrieval.201 Created: Business newly created and staged for asynchronous cache population.400 Bad Request: Invalid coordinates (e.g., latitude outside ) or radius .401 Unauthorized: Missing or invalid Bearer JWT.404 Not Found: Specificbusiness_iddoes not exist.429 Too Many Requests: Client exceeded rate limits (e.g., per IP).503 Service Unavailable: Downstream spatial storage degradation; fallback response returned with degraded radius.
json{ "error": { "code": "INVALID_GEOSPATIAL_COORDINATES", "message": "Latitude 98.412 is outside valid WGS-84 boundaries [-90.0, 90.0].", "invalid_parameters": [ { "field": "lat", "value": 98.412, "rule": "RANGE_-90_TO_90" } ], "timestamp": "2026-09-16T14:32:01Z" } }
5. Data Models & Storage Architecture
Database Selection Justification
- Amazon Aurora PostgreSQL Multi-AZ with PostGIS Extension: Master datastore for business entities and physical spatial geometries. PostGIS natively supports the
GEOGRAPHY(Point, 4326)WGS-84 coordinate reference system, accounting for Earth's curvature. R-Tree-based Generalized Search Tree (GiST) indexes provide logarithmic bounding-box search performance. - Amazon ElastiCache Redis Cluster (Cluster Mode Enabled): Serves as the high-throughput read cache. We model spatial tiles as Geohash sets (
geo:tile:<geohash_p6>) and support sub-millisecond radius filtering using Redis nativeGEOADDandGEORADIUS/GEOSEARCHcommands. - Amazon OpenSearch Service: Powers secondary text-based discovery, typo-tolerant prefix searching (e.g., "sushi", "clam chowder"), and multi-attribute facet aggregation.
1. Master PostGIS Relational Schema (Aurora PostgreSQL DDL)
sql-- Enable PostGIS spatial extensions CREATE EXTENSION IF NOT EXISTS postgis; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Table Partitioned by Country Code for Distributed Scale CREATE TABLE businesses ( business_id UUID DEFAULT uuid_generate_v4(), name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, country_code CHAR(2) NOT NULL, category_id VARCHAR(64) NOT NULL, price_tier SMALLINT CHECK (price_tier BETWEEN 1 AND 4), phone VARCHAR(32), address_street VARCHAR(255) NOT NULL, address_city VARCHAR(128) NOT NULL, address_state VARCHAR(64), postal_code VARCHAR(16), -- Spatial Geography Type: WGS 84 (SRID 4326) location GEOGRAPHY(Point, 4326) NOT NULL, -- Precalculated Geohash strings for hierarchical prefix lookups geohash_p4 CHAR(4) NOT NULL, -- ~39km x 19.5km (Regional) geohash_p5 CHAR(5) NOT NULL, -- ~4.9km x 4.9km (Suburban) geohash_p6 CHAR(6) NOT NULL, -- ~1.2km x 0.6km (Dense Urban) rating_avg NUMERIC(2, 1) DEFAULT 0.0 CHECK (rating_avg BETWEEN 0.0 AND 5.0), review_count INTEGER DEFAULT 0 CHECK (review_count >= 0), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (country_code, business_id) ) PARTITION BY LIST (country_code); -- Sample Partition for United States CREATE TABLE businesses_us PARTITION OF businesses FOR VALUES IN ('US'); -- 1. Primary PostGIS Spatial Index: Generalized Search Tree (GiST) CREATE INDEX idx_businesses_us_spatial ON businesses_us USING GIST (location); -- 2. Fast B-Tree Indexes on Geohash prefixes for direct cache-miss fill CREATE INDEX idx_businesses_us_geohash_p6 ON businesses_us (geohash_p6, is_active) INCLUDE (business_id, rating_avg); -- 3. Composite Index for Filtered Proximity Queries CREATE INDEX idx_businesses_us_cat_rating ON businesses_us (category_id, rating_avg DESC, is_active); -- Business Operational Schedules CREATE TABLE business_hours ( hours_id BIGSERIAL PRIMARY KEY, business_id UUID NOT NULL, country_code CHAR(2) NOT NULL, day_of_week SMALLINT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6), -- 0=Sunday open_time TIME NOT NULL, close_time TIME NOT NULL, is_closed BOOLEAN DEFAULT FALSE, FOREIGN KEY (country_code, business_id) REFERENCES businesses(country_code, business_id) ON DELETE CASCADE ); CREATE INDEX idx_hours_lookup ON business_hours (business_id, day_of_week);
2. PostGIS Production Spatial Query (ST_DWithin)
To query businesses within using accurate spheroidal math:
sqlSELECT business_id, name, category_id, rating_avg, review_count, ST_Y(location::geometry) AS latitude, ST_X(location::geometry) AS longitude, ST_Distance(location, ST_MakePoint(-122.419416, 37.774929)::geography) AS distance_meters FROM businesses_us WHERE ST_DWithin( location, ST_MakePoint(-122.419416, 37.774929)::geography, 2000 -- Radius in meters ) AND is_active = TRUE AND rating_avg >= 4.0 ORDER BY distance_meters ASC LIMIT 20;
3. ElastiCache Redis Key-Value & Geospatial Schema
text1. Geohash Tile Key (Set of Venue IDs): Key: geo:tile:<geohash_precision_6> (e.g., "geo:tile:9q8yyk") Type: SET Value: [ "biz_9b1deb4d...", "biz_8a2ceb3f...", ... ] TTL: 86400 seconds (24 Hours) 2. Redis Native GEO Sorted Set (Alternative Ultra-Fast Read Path): Key: geo:index:<country_code>:<geohash_p4> (e.g., "geo:index:US:9q8y") Type: ZSET (Internal 52-bit integer Geohash encoding) Command: GEOADD geo:index:US:9q8y -122.418294 37.778291 "biz_9b1deb4d..." Query: GEOSEARCH geo:index:US:9q8y FROMLONLAT -122.419416 37.774929 BYRADIUS 2000 M ASC WITHDIST WITHCOORD 3. Venue Summary Entity Hash: Key: biz:summary:<business_id> Type: HASH Fields: { "name": "Blue Bottle", "lat": 37.778291, "lon": -122.418294, "rating": 4.6, "reviews": 1842, "cat": "coffee" } TTL: 604800 seconds (7 Days)
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~39%). Spend 1 Coin to unlock the remaining 6 production deep-dive sections for a full 24 hours.