Case Study: Design a Proximity Service (Nearby Businesses)

“Mỗi ngày hàng trăm triệu người mở Google Maps hỏi: ‘Quán phở nào gần đây nhất?’ — Câu hỏi tưởng đơn giản, nhưng phía sau là một bài toán geospatial indexing phức tạp, nơi toán học gặp engineering ở quy mô hành tinh.”

Tags: system-design proximity-service geospatial geohash quadtree alex-xu case-study vol2 Student: Hieu Prerequisite: Tuan-02-Back-of-the-envelope · Tuan-06-Cache-Strategy · Tuan-07-Database-Sharding-Replication Lien quan: Tuan-05-Load-Balancer · Tuan-09-Rate-Limiter · Tuan-10-Consistent-Hashing · Tuan-13-Monitoring-Observability Reference: Alex Xu, System Design Interview — An Insider’s Guide, Volume 2 — Chapter 1: Proximity Service


1. Context & Why — Tại sao Proximity Service quan trọng?

1.1 Analogy — Bài toán đời thường

hãy tưởng tượng 2 tình huống hàng ngày:

Tình huống 1 — Google Maps: Bạn đang ở quận 1 TP.HCM, mở Google Maps và gõ “phở”. Trong vòng 0.5 giây, bản đồ hiện ra 20 quán phở gần nhất, sắp xếp theo khoảng cách. Phía sau cái “0.5 giây” đó là gì? Làm sao từ tọa độ GPS của bạn (10.7769, 106.7009), hệ thống tìm được 20 quán phở trong hàng trăm triệu địa điểm trên thế giới?

Tình huống 2 — Grab tìm tài xế: Bạn đặt Grab, app cần tìm tất cả tài xế trong bán kính 3km quanh vị trí của em. Nhưng tài xế di chuyển liên tục — vị trí của họ thay đổi mỗi 3-5 giây. Hệ thống phải trả lời câu hỏi “ai gần bạn nhất?” cho hàng triệu user đồng thời.

Cả hai tình huống này đều là bài toán Proximity Service — cho một tọa độ (latitude, longitude) và bán kính (radius), tìm tất cả các điểm (businesses, tài xế, bạn bè) trong vùng đó.

1.2 Tại sao bài toán này khó?

Vấn đềGiải thíchQuy mô
Dữ liệu khổng lồ200 triệu businesses trên toàn cầu~200M records
Two-dimensional searchKhông thể dùng B-tree index bình thường — latitude và longitude là 2 chiều độc lậpO(N) naive search
Read-heavyHàng trăm triệu user search đồng thời, nhưng businesses ít khi thay đổiRead:Write ~ 1000:1
Low latencyUser kỳ vọng kết quả trong < 500msp99 < 200ms
Global scaleUser và businesses ở khắp thế giới, mỗi region có density khác nhauTokyo dense, Sahara sparse

1.3 Real-world Systems sử dụng Proximity Service

CompanyUse CaseScale
Google Maps / PlacesTìm nhà hàng, gas station, ATM200M+ places
Uber / GrabTìm tài xế gần nhấtMillions of active drivers
YelpTìm businesses theo category + location~200M businesses
Facebook Nearby FriendsTìm bạn bè đang ở gần2B+ users
TinderTìm người match trong bán kính75M+ users
DoorDashTìm nhà hàng có delivery390K+ merchants

Step 1 — Understand the Problem & Establish Design Scope

1.1 Clarifying Questions (Câu hỏi làm rõ)

Trong interview, luôn hỏi trước khi thiết kế. Đây là các câu hỏi quan trọng:

Câu hỏiTrả lờiGhi chú
User có thể chỉ định bán kính search?Có — 0.5km, 1km, 2km, 5km, 20kmDefault 5km
Radius tối đa là bao nhiêu?20kmGiới hạn để tránh query quá rộng
Businesses có thể update thông tin?Có — nhưng thay đổi phản ánh vào ngày hôm sau là chấp nhận đượcNear real-time không bắt buộc
User có thể di chuyển và search đồng thời?Có, nhưng không cần auto-refresh liên tục — chỉ khi user bấm search lạiKhác với Nearby Friends
Cần sắp xếp kết quả theo gì?Khoảng cách, hoặc ranking/ratingDistance là primary
Business info gồm những gì?Tên, địa chỉ, tọa độ, category, giờ mở cửa, rating, ảnhRich metadata

1.2 Functional Requirements (Yêu cầu chức năng)

  • FR1: Cho tọa độ (lat, lng) và bán kính (radius), trả về danh sách businesses trong vùng đó
  • FR2: Business owners có thể thêm/sửa/xóa thông tin business (CRUD)
  • FR3: Users có thể xem chi tiết business (tên, địa chỉ, rating, giờ mở cửa, ảnh)
  • FR4: Kết quả có thể lọc theo category (nhà hàng, cafe, gas station…)

1.3 Non-functional Requirements (Yêu cầu phi chức năng)

Yêu cầuMục tiêuGiải thích
Low Latencyp99 < 200ms cho searchUser kỳ vọng kết quả nhanh
High Availability99.99% uptimeLBS là core feature của Maps
High Scalability200M DAU, 100M businessesGlobal scale
Eventual ConsistencyBusiness updates có thể delay vài phútKhông cần real-time cho business info
Read-heavyRead QPS >> Write QPSSearch nhiều hơn update

1.4 Capacity Estimation (Ước lượng năng lực)

Assumptions:

Thông sốGiá trịGiải thích
DAU200MQuy mô Google Maps
Businesses100MToàn cầu
Searches/user/day5Trung bình
Business updates/day100KThêm/sửa/xóa

QPS Calculation — Search:

Nhận xét: 60K peak QPS cho read — đây là read-heavy system. Write QPS chỉ khoảng 1-2 QPS cho business updates. Tỷ lệ Read:Write ~ 60,000:1.

Write QPS — Business Updates:

Write QPS cực thấp — hoàn toàn có thể handle bởi 1 primary database server.

Storage — Business Data:

Storage — Geospatial Index:

Quan trọng: 3.6 GB geospatial index vừa đủ để load toàn bộ vào memory. Đây là insight then chốt — ta có thể build in-memory index (Quadtree) trên 1 server.

Cache Sizing:

Sử dụng Redis cluster với vài node là đủ. Tham chiếu Tuan-06-Cache-Strategy.

Quadtree Memory Estimation (sẽ deep dive ở section 3):

Aha Moment: Quadtree cho 100M businesses chỉ chiếm khoảng 843 MB RAM — thoải mái fit trong 1 server có 8 GB RAM. Đây là lý do nhiều hệ thống chọn Quadtree làm in-memory geospatial index.

Tóm tắt Estimation

MetricValue
Search QPS (peak)~60K/s
Write QPS (peak)~5/s
Business data storage~600 GB
Geospatial index size~3.6 GB
Cache size (hot data)~120 GB
Quadtree memory~843 MB
Read:Write ratio~60,000:1

Step 2 — High-Level Design

2.1 Kiến trúc tổng quan — 2 Service chính

Proximity Service bao gồm 2 loại service có đặc tính hoàn toàn khác nhau:

ServiceChức năngĐặc tínhScale strategy
LBS (Location-Based Service)Nhận tọa độ + radius, trả về businesses gần nhấtRead-heavy, stateless, no writeHorizontal scaling, read replicas
Business ServiceCRUD operations cho businessesWrite operations, low QPSSingle primary + replicas

Tại sao tách 2 service? Vì read và write có pattern hoàn toàn khác nhau:

  • LBS: 60K QPS, stateless, cần low latency → scale horizontally, đặt sau Load Balancer
  • Business Service: 5 QPS, stateful, cần consistency → single primary database

Principle: Tách read path và write path khi chúng có scale requirements khác nhau. Đây là CQRS (Command Query Responsibility Segregation) pattern — một dạng của Tuan-11-Microservices-Pattern.

2.2 High-Level Architecture Diagram

flowchart TB
    subgraph Clients
        Mobile["Mobile App"]
        Web["Web App"]
    end

    LB["Load Balancer<br/>→ [[Tuan-05-Load-Balancer]]"]

    subgraph "Read Path (LBS)"
        LBS1["LBS Instance 1"]
        LBS2["LBS Instance 2"]
        LBSN["LBS Instance N"]
    end

    subgraph "Write Path"
        BS["Business Service"]
    end

    subgraph "Data Layer"
        Primary["Primary DB<br/>PostgreSQL"]
        R1["Read Replica 1"]
        R2["Read Replica 2"]
        RN["Read Replica N"]
    end

    subgraph "Geospatial Index"
        GI["Geohash Index<br/>(in DB read replicas)"]
        QT["Quadtree<br/>(in-memory, per LBS instance)"]
    end

    subgraph "Cache Layer"
        RC["Redis Cluster<br/>Business data cache<br/>Geohash → business_ids cache"]
    end

    Mobile --> LB
    Web --> LB
    LB -->|"GET /nearby"| LBS1 & LBS2 & LBSN
    LB -->|"POST/PUT/DELETE /businesses"| BS

    LBS1 & LBS2 & LBSN --> RC
    LBS1 & LBS2 & LBSN --> R1 & R2 & RN
    LBS1 & LBS2 & LBSN -.-> QT

    BS --> Primary
    Primary -->|"Replication"| R1 & R2 & RN

    R1 & R2 & RN --> GI

    style LB fill:#42a5f5,color:#fff
    style RC fill:#ef5350,color:#fff
    style Primary fill:#66bb6a,color:#fff
    style QT fill:#ffa726,color:#000

2.3 API Design

Search Nearby Businesses:

FieldTypeDescription
GET /v1/search/nearby
latitudedoubleVĩ độ của user (-90 đến 90)
longitudedoubleKinh độ của user (-180 đến 180)
radiusintBán kính search (meters). Default: 5000. Max: 20000
categorystring(Optional) Filter theo loại business
page_tokenstring(Optional) Pagination token

Response:

FieldTypeDescription
businessesarrayDanh sách businesses
businesses[].idstringBusiness ID
businesses[].namestringTên business
businesses[].latdoubleVĩ độ
businesses[].lngdoubleKinh độ
businesses[].distanceintKhoảng cách (meters)
businesses[].ratingfloatRating trung bình
next_page_tokenstringToken cho trang tiếp theo

Business CRUD:

EndpointMethodDescription
/v1/businessesPOSTTạo business mới
/v1/businesses/:idGETXem chi tiết business
/v1/businesses/:idPUTCập nhật business
/v1/businesses/:idDELETEXóa business

2.4 Data Flow — Read vs Write

Read Flow (Search):

1. User gửi request GET /v1/search/nearby?lat=10.77&lng=106.70&radius=5000
2. Load Balancer route đến 1 trong N LBS instances
3. LBS tính geohash từ (lat, lng) và radius → xác định các geohash cells cần query
4. LBS check Redis cache: geohash_cells → list of business_ids
5. Nếu cache hit → lấy business details từ Business Data Cache
6. Nếu cache miss → query Read Replica (geospatial index table)
7. Tính khoảng cách chính xác, lọc businesses ngoài radius
8. Sắp xếp theo distance, trả về kết quả

Write Flow (Update Business):

1. Business owner gửi PUT /v1/businesses/123
2. Load Balancer route đến Business Service
3. Business Service update Primary DB
4. Primary DB replicate sang Read Replicas (async)
5. Cache invalidation: xóa cache entries liên quan
6. Nếu dùng Quadtree: rebuild scheduled (overnight hoặc mỗi vài giờ)

Trade-off: Business updates không phản ánh ngay lập tức trong search results. Delay có thể là vài phút (replication lag) đến vài giờ (quadtree rebuild). Đây là chấp nhận được vì businesses hiếm khi thay đổi vị trí.


Step 3 — Design Deep Dive

3.1 Bài toán cốt lõi: Geospatial Indexing

đây là phần khó nhất và quan trọng nhất của bài này. Bài toán cơ bản là:

Cho điểm P(lat, lng) và bán kính r, tìm tất cả businesses B sao cho distance(P, B) r.

Cách naive — Two-dimensional search:

Cách đơn giản nhất: query trực tiếp trong database.

SELECT * FROM businesses
WHERE latitude BETWEEN 10.77 - delta AND 10.77 + delta
  AND longitude BETWEEN 106.70 - delta AND 106.70 + delta

Vấn đề: Ngay cả khi có index trên latitude VÀ index trên longitude, database phải:

  1. Query index trên latitude → tập A (có thể hàng triệu records)
  2. Query index trên longitude → tập B (có thể hàng triệu records)
  3. Lấy giao (intersection) A ∩ B → rất chậm!

Lý do: B-tree index chỉ hiệu quả cho 1 chiều. Khi có 2 chiều độc lập (lat, lng), không có B-tree nào có thể index cả 2 đồng thời một cách hiệu quả.

Đây là lý do ta cần geospatial indexing — các kỹ thuật chuyên biệt để biến bài toán 2 chiều thành bài toán 1 chiều hoặc chia không gian thành các ô (cells) để thu hẹp phạm vi search.

3.2 Approach 1 — Evenly Divided Grid

Ý tưởng: Chia toàn bộ bản đồ thành các ô (cells) có kích thước bằng nhau. Mỗi business thuộc về đúng 1 cell. Khi search, chỉ cần tìm các businesses trong cell hiện tại và các cells lân cận.

Vấn đề nghiêm trọng:

Vấn đềGiải thích
Distribution không đềuTokyo có thể có 100,000 businesses trong 1 cell, nhưng Sahara có 0
Fixed granularityKhông thể adaptive — vùng đông đúc cần grid nhỏ hơn, vùng thưa cần grid lớn hơn
Quá nhiều empty cells70% Trái Đất là nước → 70% cells rỗng
Edge casesBusiness ở sát ranh giới cell → search cần check nhiều cells

Kết luận: Evenly divided grid không phù hợp cho dữ liệu phân bố không đều. Ta cần cách tiếp cận thông minh hơn.

3.3 Approach 2 — Geohash (Core Approach)

3.3.1 Geohash là gì?

Geohash là kỹ thuật biến tọa độ 2 chiều (lat, lng) thành 1 chuỗi string duy nhất bằng cách chia đôi xen kẽ (interleaving) latitude và longitude.

Quy trình encoding (simplified):

Bước 1 — Encode Longitude:

  • Bắt đầu với khoảng [-180, 180]
  • Chia đôi: nếu lng nằm bên trái ([-180, 0]) → bit 0, bên phải ([0, 180]) → bit 1
  • Lặp lại, chia đôi tiếp khoảng được chọn
  • Ví dụ: lng = 106.70 → [0, 180] → 1, [90, 180] → 1, [90, 135] → 1, [101.25, 112.5] → 1, …

Bước 2 — Encode Latitude:

  • Tương tự, bắt đầu với [-90, 90]
  • Ví dụ: lat = 10.77 → [0, 90] → 1, [0, 45] → 0, [0, 22.5] → 0, …

Bước 3 — Interleave Bits:

  • Xen kẽ: bit lon[0], bit lat[0], bit lon[1], bit lat[1], …
  • Kết quả là 1 chuỗi bits

Bước 4 — Encode thành Base32:

  • Nhóm 5 bits → 1 ký tự Base32 (0-9, b-z loại trừ a, i, l, o)
  • Ví dụ: tọa độ TP.HCM (10.7769, 106.7009) → geohash: w3gvk1

3.3.2 Precision Levels — Độ chính xác

Geohash LengthCell WidthCell HeightSử dụng
15,009 km4,992 kmToàn bộ lục địa
21,252 km624 kmQuốc gia nhỏ
3156 km156 kmTỉnh / state
439.1 km19.5 kmThành phố lớn
54.89 km4.89 kmQuận / district
61.22 km0.61 kmKhu vực nhỏ — phù hợp cho proximity search
7153 m153 mĐường phố
838.2 m19.1 mTòa nhà
94.77 m4.77 mPhòng trong tòa nhà
123.7 cm1.8 cmĐộ chính xác GPS max

Rule of thumb cho proximity search: Radius 5km → dùng geohash length 5 (cell ~4.89 km). Radius 1km → dùng geohash length 6 (cell ~1.22 km).

3.3.3 Mapping Radius to Geohash Precision

RadiusGeohash PrecisionGiải thích
0.5 km6Cell 1.22km, cần check cell hiện tại + 8 neighbors
1 km5Cell 4.89km đủ cover
2 km5Cell 4.89km đủ cover
5 km4Cell 39.1km — đủ rộng, có thể trả về quá nhiều kết quả
20 km4Cell 39.1km cover được

Chiến lược thực tế: Chọn precision level sao cho cell size >= radius và query cell hiện tại + 8 cells lân cận (tổng cộng 9 cells). Điều này đảm bảo cover toàn bộ vùng tròn search.

3.3.4 Tính chất quan trọng của Geohash

Tính chất 1 — Shared prefix = Nearby (thường là đúng):

Hai điểm có geohash chia sẻ prefix càng dài → càng gần nhau.

Điểm AĐiểm BShared prefixKhoảng cách xấp xỉ
w3gvk1w3gvk9w3gvk (5 chars)< 5 km
w3gvk1w3gvm2w3gv (4 chars)< 40 km
w3gvk1w3h001w3 (2 chars)< 1,252 km

Tính chất 2 — Neighbor lookup:

Mỗi geohash cell có chính xác 8 neighbors (trên, dưới, trái, phải, 4 góc). Có thể tính neighbors bằng algorithm hiệu quả trong O(1).

3.3.5 Edge Cases — Những trường hợp đặc biệt

Edge Case 1 — Khác prefix nhưng rất gần (Boundary Problem):

┌──────────┬──────────┐
│          │          │
│  w3gvk   │  w3gvm   │
│          │          │
│        A ● ● B     │
│          │          │
└──────────┴──────────┘

Điểm A (w3gvk...) và B (w3gvm...) ở 2 cells khác nhau — prefix khác hoàn toàn từ ký tự thứ 5. Nhưng thực tế chúng chỉ cách nhau 50 mét!

Giải pháp: Luôn query cell hiện tại + 8 cells lân cận. Điều này đảm bảo không bỏ sót các businesses ở ranh giới cell.

Edge Case 2 — Cùng prefix nhưng xa (Rare case):

Do cách Hilbert/Z-curve map 2D → 1D, có những trường hợp 2 cells có cùng prefix nhưng không thực sự lân cận về mặt địa lý. Ví dụ: 2 cells ở 2 bên của đường ranh giới encoding.

Giải pháp: Sau khi lấy danh sách businesses từ geohash cells, luôn tính khoảng cách thực tế (Haversine formula) và lọc bỏ những businesses ngoài radius.

Trong đó = bán kính Trái Đất (6,371 km), = latitude (radians), = longitude (radians).

Edge Case 3 — Không đủ businesses trong radius:

Khi search radius = 1km nhưng chỉ có 2 businesses, user có thể muốn xem thêm. Chiến lược mở rộng radius:

1. Search với geohash precision tương ứng radius ban đầu
2. Nếu kết quả < min_results (ví dụ: 10)
3. Giảm geohash precision 1 bậc (mở rộng vùng search ~4x)
4. Search lại
5. Lặp lại cho đến khi đủ kết quả hoặc đạt max_radius

3.4 Approach 3 — Quadtree (Adaptive Grid)

3.4.1 Quadtree là gì?

Quadtree là cấu trúc dữ liệu hình cây, trong đó mỗi node có thể có 0 hoặc 4 children. Nó tự động chia nhỏ vùng có nhiều businesses và giữ nguyên vùng ít businesses.

Nguyên tắc: Chia không gian 2D thành 4 phần. Nếu 1 phần có nhiều hơn K businesses (threshold, ví dụ K=100), tiếp tục chia phần đó thành 4. Lặp lại cho đến khi mỗi ô có K businesses.

3.4.2 Quy trình Build Quadtree

Bắt đầu: Toàn bộ bản đồ là 1 node
  - Node chứa 5,000,000 businesses (> threshold 100) → SPLIT

Node chia thành 4 children:
  NW (Northwest): 1,200,000 businesses → SPLIT
  NE (Northeast): 2,500,000 businesses → SPLIT
  SW (Southwest): 800,000 businesses → SPLIT
  SE (Southeast): 500,000 businesses → SPLIT

Tiếp tục chia cho đến khi mỗi leaf node có <= 100 businesses

Đặc điểm quan trọng:

Đặc điểmGiải thích
AdaptiveVùng Tokyo (đông đúc) có cell nhỏ ~100m. Vùng Sahara có cell lớn ~1000km
In-memoryToàn bộ tree nằm trong RAM — query cực nhanh
Build timeBuild 1 lần khi server khởi động, hoặc rebuild định kỳ
Not easily shareableMỗi server giữ 1 bản copy của tree — không chia sẻ qua network

3.4.3 Quadtree Structure — Chi tiết Node

Internal Node (node có children):

FieldSizeGiải thích
Top-left coordinates16 bytes(lat, lng) góc trên trái
Bottom-right coordinates16 bytes(lat, lng) góc dưới phải
Pointer to NW child8 bytes
Pointer to NE child8 bytes
Pointer to SW child8 bytes
Pointer to SE child8 bytes
Total64 bytes

Leaf Node (node chứa businesses):

FieldSizeGiải thích
Top-left coordinates16 bytesRanh giới vùng
Bottom-right coordinates16 bytesRanh giới vùng
List of business_ids8 bytes x NN threshold (100)
Total32 + 8N bytesVới N=100: ~832 bytes

3.4.4 Memory Estimation (Chi tiết)

Quadtree là full 4-ary tree. Tổng số internal nodes:

Key insight: 853 MB — một server với 8 GB RAM có thể chứa thoải mái. Không cần distributed index! Đây là điều nhiều ứng viên interview không nhận ra.

3.4.5 Build Time Estimation

Với máy tính hiện đại xử lý ~1 billion operations/second, build time khoảng vài phút. Có thể chấp nhận cho startup time.

3.4.6 Quadtree Search Algorithm

Tìm businesses trong radius R từ điểm P(lat, lng):

1. Bắt đầu từ root node
2. Tìm leaf node chứa điểm P (traverse tree theo tọa độ)
3. Tính khoảng cách từ P đến mỗi business trong leaf node đó
4. Nếu vòng tròn (P, R) overlap với các neighboring cells:
   - Traverse lên parent, check các sibling nodes
   - Lặp lại cho đến khi vòng tròn được cover hoàn toàn
5. Lọc businesses có distance > R
6. Sắp xếp theo distance, trả về top N

Trong đó = số businesses trong vùng search. Với :

3.4.7 Update Strategy cho Quadtree

Vấn đề: Businesses thay đổi (thêm mới, đóng cửa, đổi địa chỉ) — làm sao update Quadtree?

Option 1 — Rebuild định kỳ (Recommended):

AspectDetail
FrequencyMỗi vài giờ hoặc overnight
ProcessBuild quadtree mới từ database, swap với tree cũ (atomic swap)
DowntimeZero — tree cũ vẫn serve requests trong khi tree mới đang build
Phù hợp khiBusiness data thay đổi chậm (hàng ngày), không cần real-time

Option 2 — Incremental update:

AspectDetail
ProcessThêm/xóa business trực tiếp trong tree
ComplexityCao — cần handle split/merge khi node vượt threshold
Phù hợp khiData thay đổi thường xuyên (ví dụ: driver locations trong Uber)

Cho bài toán Proximity Service (businesses), Option 1 là đủ vì businesses hiếm khi thay đổi vị trí.

3.5 Approach 4 — Google S2 Geometry Library

3.5.1 S2 là gì?

Google S2 là thư viện chuyên dụng do Google phát triển, sử dụng Hilbert curve để map bề mặt hình cầu (Trái Đất) thành các S2 cells.

Điểm khác biệt với Geohash:

FeatureGeohashS2
Hình dạng cellHình chữ nhật trên bản đồHình thoi (rhombus) trên hình cầu
Mô hình Trái ĐấtPhẳng (flat projection)Hình cầu (sphere)
Curve typeZ-order curveHilbert curve
Cell levels1-12 (base32 chars)0-30 (64-bit integer)
Độ chính xácTốt, có boundary issuesTốt hơn — Hilbert curve bảo toàn locality tốt hơn
Region coveringManual (9 cells)Tự động — S2RegionCoverer tìm tối ưu cells

3.5.2 Hilbert Curve — Tại sao tốt hơn Z-curve?

Z-curve (Geohash) có vấn đề “jumps” — đôi khi 2 cells kề nhau trên bản đồ lại xa nhau trên Z-curve. Điều này gây ra các boundary issues.

Hilbert curve đảm bảo: 2 điểm gần nhau trên curve hầu như luôn gần nhau trong không gian 2D. Tính chất này gọi là locality-preserving — tốt hơn Z-curve đáng kể.

3.5.3 S2 trong thực tế

CompanySử dụng S2 cho
Google MapsCore geospatial indexing
Uber H3Inspired by S2, dùng hexagonal cells
FoursquarePlace detection
MongoDB2dsphere index sử dụng S2 internally

3.6 So sánh Geohash vs Quadtree vs S2

Tiêu chíGeohashQuadtreeS2
Độ phức tạp implementThấp — chỉ là string operationTrung bình — cần build treeCao — cần S2 library
StorageTrong database (column)In-memory trên mỗi serverTrong database (column)
UpdateUpdate database recordRebuild treeUpdate database record
Adaptive densityKhông — fixed grid sizeCó — tự động chia nhỏ vùng đông đúcCó — S2RegionCoverer
Precision control12 levels (base32)Vô hạn (chia cho đến khi đủ)31 levels (0-30)
Boundary issuesCó — cần query 8 neighborsÍt — tree structure handle tự nhiênÍt nhất — Hilbert curve
DistributedDễ — geohash là string, index bình thườngKhó — tree phải ở 1 serverDễ — S2 cell ID là integer
Database supportRedis, PostgreSQL, MySQL, DynamoDBPhải tự implementMongoDB (native), PostgreSQL (extension)
Phù hợp choHầu hết use cases, đơn giảnDataset lớn, cần adaptive, in-memory okGoogle-scale, hình cầu chính xác

Recommendation cho interview: Chọn Geohash làm primary approach (dễ giải thích, database support tốt). Nhắc đến Quadtree như alternative khi interviewer hỏi về adaptive density. Nhắc đến S2 để show depth of knowledge.

3.7 Database Schema Design

3.7.1 Business Table

ColumnTypeDescriptionIndex
business_idUUID (PK)ID duy nhấtPrimary Key
nameVARCHAR(255)Tên businessFull-text index
addressTEXTĐịa chỉ
cityVARCHAR(100)Thành phố
countryVARCHAR(2)Mã quốc gia (ISO 3166)
latitudeDOUBLEVĩ độ
longitudeDOUBLEKinh độ
categoryVARCHAR(50)Loại businessIndex
ratingDECIMAL(2,1)Rating trung bình
phoneVARCHAR(20)Số điện thoại
hoursJSONBGiờ mở cửa
photosJSONBURLs ảnh
created_atTIMESTAMP
updated_atTIMESTAMPIndex

3.7.2 Geospatial Index Table (Geohash approach)

ColumnTypeDescriptionIndex
geohashVARCHAR(12)Geohash của businessPrimary prefix index
business_idUUIDFK to business table
latitudeDOUBLEĐể tính khoảng cách chính xác
longitudeDOUBLEĐể tính khoảng cách chính xác

Index Strategy: Tạo compound index trên (geohash, business_id). Khi query, dùng prefix search:

-- Tìm businesses trong geohash cell "w3gvk" và các neighbors
WHERE geohash LIKE 'w3gvk%'
   OR geohash LIKE 'w3gvm%'
   OR geohash LIKE 'w3gvj%'
   ... (tổng 9 cells)

Optimization: Thay vì 9 queries riêng biệt, dùng IN clause hoặc UNION ALL để batch thành 1 query.

3.7.3 Database Architecture — Read Replicas

flowchart LR
    subgraph "Write Path"
        BS["Business Service"]
        Primary["Primary DB<br/>PostgreSQL"]
    end

    subgraph "Read Path"
        LBS["LBS Instances"]
        R1["Read Replica 1<br/>US-West"]
        R2["Read Replica 2<br/>US-East"]
        R3["Read Replica 3<br/>EU-West"]
        R4["Read Replica 4<br/>AP-Southeast"]
    end

    BS --> Primary
    Primary -->|"Async Replication"| R1 & R2 & R3 & R4
    LBS --> R1 & R2 & R3 & R4

    style Primary fill:#66bb6a,color:#fff
    style R1 fill:#42a5f5,color:#fff
    style R2 fill:#42a5f5,color:#fff
    style R3 fill:#42a5f5,color:#fff
    style R4 fill:#42a5f5,color:#fff

Tại sao Read Replicas theo region? Vì LBS là read-heavy (60K QPS peak). Mỗi region có read replicas riêng → giảm latency (user ở Singapore query replica ở Singapore, không phải đi qua US). Tham chiếu Tuan-07-Database-Sharding-Replication.

3.8 Caching Strategy

3.8.1 Cache Architecture

Sử dụng 2 lớp cache:

Cache Layer 1 — Geohash to Business IDs:

KeyValueTTLGiải thích
geo:w3gvk:restaurant[id1, id2, id3, ...]1 hourDanh sách business IDs trong geohash cell + category
geo:w3gvk:*[id1, id2, ...]1 hourTất cả businesses trong cell (không filter category)

Cache Layer 2 — Business Details:

KeyValueTTLGiải thích
biz:uuid-123{name, lat, lng, rating, ...}24 hoursChi tiết business

3.8.2 Cache Flow

Search request: lat=10.77, lng=106.70, radius=5000, category=restaurant

1. Tính geohash: w3gvk (precision 5)
2. Tính 8 neighbors: w3gvm, w3gvj, w3gvh, w3gvs, w3gvt, w3gve, w3gv7, w3gv6
3. Với mỗi geohash cell:
   a. Check Redis: GET geo:w3gvk:restaurant
   b. Cache HIT → nhận list business_ids
   c. Cache MISS → query Read Replica → set cache
4. Merge tất cả business_ids từ 9 cells
5. Với mỗi business_id:
   a. Check Redis: GET biz:uuid-123
   b. Cache HIT → nhận business details
   c. Cache MISS → query Read Replica → set cache
6. Tính distance, filter, sort, return

3.8.3 Cache Invalidation

EventActionLý do
Business update (name, hours, rating)Invalidate biz:{id}Chi tiết thay đổi
Business move (lat/lng thay đổi)Invalidate biz:{id} + geo:{old_hash}:* + geo:{new_hash}:*Vị trí thay đổi → geohash cell thay đổi
Business deletedInvalidate biz:{id} + geo:{hash}:*Xóa khỏi tất cả cache
TTL expiryTự độngSafety net

Tham chiếu Tuan-06-Cache-Strategy cho các pattern cache invalidation chi tiết.

3.9 Scaling Strategy

3.9.1 LBS — Stateless Horizontal Scaling

LBS instances là stateless (không giữ state nào). Mỗi request có thể được xử lý bởi bất kỳ instance nào.

Đặt sau Load Balancer (Tuan-05-Load-Balancer), sử dụng round-robin hoặc least-connections.

Nếu dùng Quadtree: Mỗi LBS instance giữ 1 bản copy của Quadtree trong memory. Khi scale thêm instance → instance mới build Quadtree từ database khi khởi động (vài phút). Sau đó serve requests bình thường.

3.9.2 Database — Read Replicas per Region

RegionRead ReplicasLý do
US-West3California, tech hub
US-East3New York, Washington DC
EU-West2London, Paris
AP-Southeast3Singapore, TP.HCM, Jakarta — high density
AP-Northeast2Tokyo, Seoul

PostgreSQL có thể handle 10K+ simple reads/s → 4.6K QPS per replica là thoải mái.

3.9.3 Cache — Redis Cluster

Sử dụng Redis Cluster với sharding theo geohash prefix. Tất cả keys với geohash bắt đầu “w3” (Vietnam) sẽ ở cùng shard → locality tốt cho cache.


Query Flow — End to End

sequenceDiagram
    participant U as User (Mobile)
    participant LB as Load Balancer
    participant LBS as LBS Instance
    participant Cache as Redis Cache
    participant DB as Read Replica

    U->>LB: GET /v1/search/nearby<br/>lat=10.77, lng=106.70, radius=5000

    LB->>LBS: Route to LBS Instance

    Note over LBS: 1. Tính geohash: w3gvk (precision 5)
    Note over LBS: 2. Tính 8 neighbors

    loop Cho mỗi 9 geohash cells
        LBS->>Cache: GET geo:w3gvk:*
        alt Cache HIT
            Cache-->>LBS: [business_id_1, business_id_2, ...]
        else Cache MISS
            LBS->>DB: SELECT business_id FROM geo_index<br/>WHERE geohash LIKE 'w3gvk%'
            DB-->>LBS: [business_id_1, business_id_2, ...]
            LBS->>Cache: SET geo:w3gvk:* (TTL 1h)
        end
    end

    Note over LBS: 3. Merge business_ids từ 9 cells

    loop Cho mỗi business_id
        LBS->>Cache: GET biz:{id}
        alt Cache HIT
            Cache-->>LBS: {name, lat, lng, rating, ...}
        else Cache MISS
            LBS->>DB: SELECT * FROM businesses<br/>WHERE id = ?
            DB-->>LBS: {name, lat, lng, rating, ...}
            LBS->>Cache: SET biz:{id} (TTL 24h)
        end
    end

    Note over LBS: 4. Tính Haversine distance
    Note over LBS: 5. Filter distance > radius
    Note over LBS: 6. Sort by distance

    LBS-->>LB: {businesses: [...], next_page_token: ...}
    LB-->>U: 200 OK

Geohash Grid Visualization

graph TB
    subgraph "Geohash Grid — TP.HCM Area (Precision 5)"
        subgraph row1[""]
            C1["w3gv6<br/>0 biz"]
            C2["w3gv7<br/>12 biz"]
            C3["w3gve<br/>5 biz"]
        end
        subgraph row2[""]
            C4["w3gvj<br/>45 biz"]
            C5["w3gvk<br/>●USER<br/>78 biz"]
            C6["w3gvm<br/>23 biz"]
        end
        subgraph row3[""]
            C7["w3gvh<br/>8 biz"]
            C8["w3gvs<br/>156 biz"]
            C9["w3gvt<br/>34 biz"]
        end
    end

    style C5 fill:#ff9800,color:#000,stroke:#e65100,stroke-width:3px
    style C4 fill:#c8e6c9
    style C6 fill:#c8e6c9
    style C8 fill:#c8e6c9
    style C2 fill:#c8e6c9
    style C1 fill:#e8eaf6
    style C3 fill:#e8eaf6
    style C7 fill:#e8eaf6
    style C9 fill:#e8eaf6

User ở cell w3gvk. Hệ thống query cell này + 8 neighbors (tổng 9 cells, 361 businesses). Sau đó tính khoảng cách thực và lọc.


Quadtree Structure Visualization

graph TD
    Root["ROOT<br/>Toàn bộ bản đồ<br/>100M businesses"]

    Root --> NW["NW<br/>North America<br/>25M biz"]
    Root --> NE["NE<br/>Europe + Africa<br/>20M biz"]
    Root --> SW["SW<br/>South America<br/>5M biz"]
    Root --> SE["SE<br/>Asia + Oceania<br/>50M biz"]

    SE --> SE_NW["SE-NW<br/>East Asia<br/>20M biz"]
    SE --> SE_NE["SE-NE<br/>Pacific<br/>2M biz"]
    SE --> SE_SW["SE-SW<br/>Southeast Asia<br/>18M biz"]
    SE --> SE_SE["SE-SE<br/>Oceania<br/>10M biz"]

    SE_SW --> VN1["VN-North<br/>Ha Noi area<br/>500K biz"]
    SE_SW --> VN2["VN-South<br/>TP.HCM area<br/>800K biz"]
    SE_SW --> TH["Thailand<br/>1.2M biz"]
    SE_SW --> ID["Indonesia<br/>2M biz"]

    VN2 --> D1["Quan 1<br/>5K biz"]
    VN2 --> D2["Quan 7<br/>3K biz"]
    VN2 --> D3["Thu Duc<br/>2K biz"]
    VN2 --> D4["Binh Thanh<br/>4K biz"]

    D1 --> L1["Block A<br/>95 biz ✓"]
    D1 --> L2["Block B<br/>78 biz ✓"]
    D1 --> L3["Block C<br/>88 biz ✓"]
    D1 --> L4["Block D<br/>62 biz ✓"]

    style Root fill:#e53935,color:#fff
    style SE fill:#ff9800,color:#000
    style SE_SW fill:#ffc107,color:#000
    style VN2 fill:#66bb6a,color:#fff
    style D1 fill:#42a5f5,color:#fff
    style L1 fill:#a5d6a7,stroke:#388e3c
    style L2 fill:#a5d6a7,stroke:#388e3c
    style L3 fill:#a5d6a7,stroke:#388e3c
    style L4 fill:#a5d6a7,stroke:#388e3c

Leaf nodes (màu xanh lá) chứa 100 businesses. Vùng đông đúc (Quận 1 TP.HCM) có cells nhỏ. Vùng thưa (Pacific Ocean) có cells lớn. Đây là sức mạnh của adaptive grid.


4. Security

4.1 Location Privacy — Bảo vệ vị trí người dùng

Mối đe dọaGiải phápChi tiết
Location fuzzingLàm mờ tọa độ GPS trước khi logThêm noise ±100-500m vào tọa độ trước khi lưu vào analytics/logs. User thực nhận kết quả chính xác, nhưng server logs chỉ lưu vị trí xấp xỉ
AggregationChỉ lưu aggregated dataThay vì log “User A ở tọa độ X lúc 14:00”, log “50 users search khu vực geohash w3gvk lúc 14:00-15:00”
Retention policyXóa location data sau thời gian nhất địnhRaw GPS coordinates xóa sau 30 ngày. Chỉ giữ aggregated stats
User consentHỏi quyền trước khi truy cập GPSMobile OS (iOS/Android) bắt buộc hỏi permission. Backend phải tôn trọng “While Using” vs “Always”
Encryption in transitTLS 1.3 cho mọi requestTọa độ truyền qua HTTPS, không bao giờ HTTP

4.2 Rate Limiting on Location Queries

TierLimitLý do
Per user30 requests/minuteNgười bình thường search ~5 lần/phút max
Per IP100 requests/minuteCho shared networks (co-working spaces)
Per API key (business)1,000 requests/minuteCho third-party integrations
Burst allowance2x limit trong 10 giâyCho phép burst ngắn khi user scroll bản đồ nhanh

Sử dụng Token Bucket algorithm. Tham chiếu Tuan-09-Rate-Limiter.

4.3 Chống Data Scraping từ Đối thủ

Mối đe dọa: Đối thủ có thể viết bot để scrape toàn bộ danh sách businesses và tọa độ từ API.

Biện phápChi tiết
API key requiredMỗi client cần API key. Revoke key nếu phát hiện scraping pattern
Pagination limitTối đa 20 results per page, tối đa 100 pages per session
Honeypot businessesChèn fake businesses (chỉ visible qua API, không hiện trên UI). Nếu đối thủ hiển thị honeypot → có bằng chứng scraping
Request pattern analysisPhát hiện: systematic grid scanning (search mỗi 1km trên toàn thành phố), unusual QPS, sequential page requests
CAPTCHATrigger CAPTCHA sau 50 searches liên tiếp không có user interaction
Response watermarkingChèn metadata ẩn vào response (user ID, timestamp) để trace nguồn leak

4.4 GDPR Compliance cho Location Data

Yêu cầu GDPRImplementation
Right to be forgottenUser yêu cầu xóa → xóa tất cả location history, search history, cached data
Data portabilityExport location history dạng JSON/CSV khi user yêu cầu
Purpose limitationLocation data chỉ dùng cho search nearby — không bán cho third-party advertising
Data minimizationChỉ lưu tọa độ khi cần thiết. Không lưu location history cho tính năng search (chỉ cần real-time)
ConsentExplicit opt-in cho location tracking. Granular permissions: “Chỉ khi đang dùng app”
Data Processing AgreementVới cloud provider (AWS/GCP) — đảm bảo data ở region phù hợp (EU data ở EU region)

5. DevOps & Monitoring

5.1 Key Metrics to Monitor

5.1.1 Search Latency by Region

MetricAlert ThresholdDashboard
search_latency_p50 by region> 50msTime series, group by region
search_latency_p99 by region> 200msTime series, group by region
search_latency_p99 global> 500msSingle number, red/green
search_error_rate> 0.1%Percentage gauge

Tại sao by region? Vì latency phụ thuộc vào: khoảng cách đến read replica, business density (vùng đông đúc trả về nhiều kết quả hơn → chậm hơn), và cache hit ratio.

5.1.2 Cache Performance

MetricAlert ThresholdÝ nghĩa
cache_hit_ratio_geo (geohash → business_ids)< 90%Cache không hiệu quả → check TTL, check nếu có geohash precision thay đổi
cache_hit_ratio_biz (business details)< 95%Business data ít thay đổi, cache hit phải cao
cache_eviction_rate> 1000/sRedis hết memory → cần thêm node
cache_latency_p99> 5msRedis chậm → check network, check memory fragmentation

Cache Hit Ratio by Geohash Precision:

PrecisionExpected Hit RatioGiải thích
4 (39km cells)98%+Cell lớn → nhiều user search cùng cell → cache warm
5 (4.9km cells)95%+Phổ biến nhất — balance giữa granularity và cache efficiency
6 (1.2km cells)85-90%Cell nhỏ → nhiều cells → cache cold cho cells ít search
7 (153m cells)70-80%Quá granular cho proximity search → không khuyến nghị

5.1.3 Database Replication Lag

MetricAlert ThresholdImpact
replication_lag_seconds per replica> 30sBusiness updates chậm hiển thị
replication_lag_seconds max> 300s (5 min)Cần investigate — replica có thể bị stuck
replica_connections< expectedReplica bị disconnect → data stale

Tham chiếu Tuan-07-Database-Sharding-Replication cho chi tiết về replication monitoring.

5.1.4 Quadtree Health (nếu sử dụng)

MetricAlert ThresholdÝ nghĩa
quadtree_build_time_seconds> 600 (10 min)Build chậm → data tăng hoặc server chậm
quadtree_memory_bytes> 2 GBTree lớn bất thường → check data quality
quadtree_last_build_timestamp> 6 hours agoQuadtree outdated → trigger rebuild
quadtree_max_depth> 20Có thể có dữ liệu bất thường (nhiều businesses cùng tọa độ)

5.2 Deployment Strategy

AspectStrategyLý do
LBS instancesRolling updateStateless → có thể update từng instance
Quadtree rebuildBlue-greenBuild tree mới, swap khi xong. Zero downtime
Database schema migrationOnline DDL (pt-online-schema-change)Không lock table
Cache warmingPre-warm after deployScript query top geohash cells để warm cache
Canary deployment5% traffic trướcPhát hiện vấn đề sớm trước khi rollout 100%

5.3 Alerting Rules

AlertConditionSeverityAction
High search latencyp99 > 500ms for 5 minP1On-call investigate, check DB/cache
Cache cluster down> 1 node unreachable for 2 minP1Auto-failover, page on-call
Replication lag high> 5 min for any replicaP2Check replica health, network
Quadtree build failedBuild process crashedP2Serve với tree cũ, investigate
Error rate spike> 1% for 3 minP1Check recent deploy, rollback if needed
Scraping detected> 500 sequential searches from 1 IPP3Auto rate limit, review

Tham chiếu Tuan-13-Monitoring-Observability cho alerting best practices.


6. Aha Moments & Pitfalls

6.1 Aha Moments — Những điều bất ngờ

Aha 1: Geohash Boundary Problem

Vấn đề: Hai quán phở cách nhau 50m nhưng ở 2 geohash cells khác nhau. Nếu chỉ query 1 cell → bỏ sót kết quả.

Giải pháp: Luôn query 9 cells (cell hiện tại + 8 neighbors). Chi phí: 9x queries thay vì 1x. Nhưng với cache, chi phí này giảm đáng kể.

Tủi interview: Nếu interviewer hỏi “geohash có vấn đề gì?”, PHẢI nhắc đến boundary problem và giải pháp 9-cell query. Đây là điểm phân biệt ứng viên trung bình và ứng viên tốt.

Aha 2: Quadtree fit trong 1 server

100 triệu businesses, quadtree chỉ chiếm ~853 MB RAM. Một server có 16 GB RAM đủ sức giữ cả tree. Không cần distributed index.

Ý nghĩa: Khi interviewer gợi ý “làm sao distribute quadtree across servers?”, câu trả lời đúng là: không cần distribute. Mỗi LBS server giữ 1 bản copy. Scale bằng cách thêm server (mỗi server có full copy).

So sánh với B-tree: Nếu dùng B-tree trong database, index cho 100M records có thể chiếm 10-20 GB (vì B-tree có overhead lớn hơn). Quadtree tiết kiệm memory hơn đáng kể cho geospatial data.

Aha 3: Radius không map 1-1 với Geohash Precision

Không có công thức chính xác: radius = X → geohash precision = Y. Phải chọn precision sao cho cell size >= radius, rồi query 9 cells.

Ví dụ: Radius 2km. Cell precision 6 = 1.22km (nhỏ hơn radius) → không đủ. Cell precision 5 = 4.89km (lớn hơn radius) → OK, nhưng trả về nhiều kết quả thừa. Giải pháp: dùng precision 5, query 9 cells, rồi filter bằng Haversine distance.

Aha 4: Write path và Read path hoàn toàn tách biệt

Proximity Service là một trong những case study rõ ràng nhất của CQRS pattern. Write QPS = 1-5, Read QPS = 60,000. Scale chung sẽ lãng phí tài nguyên.

Aha 5: Geohash là string → dùng được với mọi database

Geohash chỉ là 1 chuỗi string (ví dụ: “w3gvk1”). Có thể lưu trong bất kỳ database nào có string index: PostgreSQL, MySQL, DynamoDB, Redis, Cassandra. Không cần database đặc biệt.

6.2 Pitfalls — Những sai lầm phổ biến

Pitfall 1: Dùng two-dimensional search trực tiếp

“SELECT WHERE lat BETWEEN … AND lng BETWEEN …” — chậm O(N) vì database không thể dùng cả 2 index cùng lúc hiệu quả.

Fix: Dùng geospatial index (geohash, quadtree, hoặc PostGIS).

Pitfall 2: Chỉ query 1 geohash cell

Bỏ qua boundary problem → mất kết quả ở ranh giới cell. Đây là lỗi nghiêm trọng nhất khi implement geohash search.

Fix: Luôn query cell hiện tại + 8 neighbors.

Pitfall 3: Không tính Haversine distance sau khi query

Geohash query trả về hình vuông (hoặc hình chữ nhật). Nhưng user search theo vòng tròn (radius). Các businesses ở góc của hình vuông có thể nằm ngoài vòng tròn nhưng vẫn được trả về.

Fix: Sau khi lấy kết quả từ geohash cells, PHẢI tính Haversine distance và lọc bỏ businesses ngoài radius.

Pitfall 4: Overcomplicate distributed Quadtree

Nhiều ứng viên cố gắng distribute quadtree across servers. Không cần thiết — 853 MB fit trong 1 server. Complexity của distributed tree không đáng.

Fix: Mỗi server giữ full copy. Scale bằng cách thêm servers.

Pitfall 5: Quên cache invalidation khi business di chuyển

Khi business thay đổi tọa độ (ví dụ: food truck), geohash thay đổi. Nếu không invalidate cache cũ → user ở vị trí cũ vẫn thấy business đó, user ở vị trí mới không thấy.

Fix: Invalidate cache ở CẢ HAI geohash cũ và mới khi business update tọa độ.

Pitfall 6: Dùng geohash precision quá cao

Precision 7 (153m cells) nghe có vẻ “chính xác hơn”. Nhưng cell nhỏ → cần query nhiều cells hơn để cover radius → nhiều queries hơn → chậm hơn. Và cache hit ratio giảm vì quá nhiều distinct cells.

Fix: Chọn precision phù hợp với radius. Thường là precision 4-6 cho hầu hết use cases.


7. Summary — Tóm tắt Decision Framework

7.1 Khi nào dùng Geohash?

Tình huốngRecommendation
Database có sẵn (PostgreSQL, MySQL)Geohash — chỉ cần thêm 1 column
Cần distributed indexGeohash — string column, dùng được với sharding
Team nhỏ, cần ship nhanhGeohash — đơn giản nhất
Data thay đổi thường xuyênGeohash — update record là xong

7.2 Khi nào dùng Quadtree?

Tình huốngRecommendation
Cần adaptive densityQuadtree — tự động chia nhỏ vùng đông đúc
Query in-memory là chấp nhậnQuadtree — cực nhanh, O(log N)
Data ít thay đổi (businesses)Quadtree — rebuild định kỳ là đủ
Single region deploymentQuadtree — không cần distribute

7.3 Khi nào dùng S2?

Tình huốngRecommendation
Cần độ chính xác hình cầuS2 — tốt nhất cho bài toán toàn cầu
Google-scaleS2 — proven at Google
MongoDB ecosystemS2 — native support (2dsphere)
Complex region queriesS2 — S2RegionCoverer tối ưu

Kiến thức nền tảng liên quan

TopicLinkÁp dụng trong Proximity Service
Cache StrategyTuan-06-Cache-StrategyGeohash cache, business data cache, invalidation patterns
Database Sharding & ReplicationTuan-07-Database-Sharding-ReplicationRead replicas per region, replication lag monitoring
Load BalancerTuan-05-Load-BalancerDistribute traffic across stateless LBS instances
Rate LimiterTuan-09-Rate-LimiterChống scraping, protect LBS từ traffic spikes
MonitoringTuan-13-Monitoring-ObservabilitySearch latency, cache hit ratio, replication lag alerts
Back-of-envelopeTuan-02-Back-of-the-envelopeQPS, storage, memory estimation
Microservices PatternTuan-11-Microservices-PatternCQRS — tách Read (LBS) và Write (Business Service)

So sánh với các Case Study khác

Case StudyĐiểm tương đồngĐiểm khác biệt
Tuan-16-Design-URL-ShortenerRead-heavy, caching quan trọngURL shortener không có geospatial component
Tuan-18-Design-News-FeedFan-out pattern, cachingNews Feed là social graph, không phải location
Tuan-20-Design-Key-Value-StoreIn-memory data structures, partitioningKV Store là general purpose, Proximity là domain-specific

9. Interview Cheat Sheet

Bước 1 — Hỏi Clarifying Questions (2-3 phút)

  • Scale: bao nhiêu businesses? bao nhiêu DAU?
  • Radius: user có chọn radius không? Max radius?
  • Real-time: business updates cần hiển thị ngay không?
  • Features: search only? hay cả CRUD businesses?

Bước 2 — High-level Design (5-7 phút)

  • Vẽ 2 services: LBS (read) + Business Service (write)
  • Vẽ database: Primary + Read Replicas
  • Nhắc CQRS pattern
  • Vẽ cache layer (Redis)

Bước 3 — Deep Dive (15-20 phút)

  • Giải thích geohash: encoding, precision levels, 9-cell query
  • Nhắc boundary problem và giải pháp
  • Quadtree như alternative: adaptive, in-memory, 853 MB
  • Database schema: business table + geospatial index table
  • Caching: 2-layer (geohash → IDs, ID → details)

Bước 4 — Wrap Up (3-5 phút)

  • Scaling: LBS stateless → horizontal, DB read replicas per region
  • Monitoring: search latency, cache hit ratio, replication lag
  • Security: location privacy, rate limiting, anti-scraping

Câu hỏi interviewer thường hỏi

Câu hỏiCách trả lời
”Geohash vs Quadtree?”Geohash: distributed, database-friendly. Quadtree: adaptive, in-memory, faster query. Chọn theo use case
”Geohash có vấn đề gì?”Boundary problem — 2 điểm gần nhau nhưng khác cell. Fix: query 9 cells
”Quadtree có scale không?“100M businesses chỉ 853 MB — mỗi server giữ full copy. Scale horizontal
”Làm sao handle business updates?”CQRS: write vào Primary, async replicate. Quadtree rebuild định kỳ. Cache invalidation
”Làm sao giảm latency?”Cache (2-layer), read replicas per region, geohash precision tuning

“Proximity Service là bài toán tuyệt vời để hiểu geospatial indexing — nền tảng của mọi ứng dụng location-based. Khi bạn hiểu cách biến bài toán 2 chiều thành 1 chiều (geohash) hoặc chia không gian thông minh (quadtree), bạn có thể áp dụng cho bất kỳ bài toán location nào: tìm tài xế, tìm bạn bè, tìm nhà hàng, hay tìm bất kỳ thứ gì trên bản đồ.”