Harbor

Ứng dụng gọi xe

KhóDữ liệu vị tríGeo indexKhoá phân tánIdempotencyGiống Grab, Be, Uber

Hiểu đề

Ứng dụng gọi xe là gì? Khách mở app, thấy xe quanh mình, đặt chuyến và được ghép với một tài xế gần đó. Tài xế liên tục gửi vị trí, nên đây là hệ thống ghi rất nhiều và cần ghép nhanh mà không giao một tài xế cho hai khách.

Yêu cầu chức năng

  1. Tài xế bật trạng thái nhận chuyến và app tự gửi vị trí khoảng 4 giây một lần
  2. Khách xem xe đang trống quanh mình và giá dự kiến trước khi đặt
  3. Khách đặt xe, hệ thống mời tài xế phù hợp gần nhất, tài xế nhận hoặc từ chối trong vài giây
  4. Trong chuyến, hai bên thấy vị trí xe và ETA cập nhật liên tục
  5. Kết thúc chuyến thì tính tiền và thu qua thẻ, ví điện tử hoặc tiền mặt

Yêu cầu phi chức năng

  • Tìm tài xế gần p95 < 200 ms
  • Vị trí xe trên màn hình khách trễ không quá 5 s
  • Ghép xong tài xế trong < 30 s cho 95% yêu cầu ở giờ cao điểm
  • 0 trường hợp một tài xế nhận 2 chuyến cùng lúc, 0 lần trừ tiền 2 lần cho một chuyến
  • Luồng đặt xe sẵn sàng 99,95% (≈ 22 phút downtime/tháng)

Ngoài phạm vi: Giao đồ ăn, giao hàng, đi chung nhiều khách; Tự xây bản đồ và định tuyến (dùng Maps API có sẵn); Đăng ký tài xế, KYC, chấm điểm tài xế; Phát hiện gian lận chi tiết.

Thực thể và API

Rider
Khách: id, tên, số điện thoại, phương thức thanh toán mặc định.
Driver
Tài xế: id, loại xe (xe máy, ô tô 4 chỗ, 7 chỗ), biển số, trạng thái OFFLINE / AVAILABLE / OFFERED / ON_TRIP.
DriverLocation
Vị trí mới nhất của tài xế: lat, lng, hướng, thời điểm. Chỉ sống trong bộ nhớ (Redis), hết hạn sau 30 s nếu tài xế mất sóng.
FareQuote
Báo giá đã chốt cho khách: điểm đón, điểm đến, giá, hệ số surge, hết hạn sau 5 phút.
Trip
Chuyến đi: riderId, driverId, quoteId, trạng thái (REQUESTED, MATCHING, ACCEPTED, ARRIVED, IN_PROGRESS, COMPLETED, CANCELLED), version.
Payment
Giao dịch thu tiền gắn với tripId, có idempotency key, trạng thái AUTHORIZED / CAPTURED / FAILED / REFUNDED.
MethodEndpointGhi chú
WS/v1/driver/streamTài xế gửi vị trí 4 s/lần qua WebSocket và nhận lời mời chuyến trên cùng kết nối.
GET/v1/drivers/nearby?lat=&lng=&type=Trả tối đa 20 xe trống gần nhất để vẽ lên bản đồ. Toạ độ được làm tròn cho khách.
POST/v1/fare-quotesGửi điểm đón, điểm đến, loại xe. Trả quoteId, giá, ETA đón, hệ số surge.
POST/v1/tripsHeader Idempotency-Key, body có quoteId. Trả 201 với trip ở trạng thái MATCHING.
POST/v1/offers/{offerId}/acceptTài xế nhận chuyến. Trả 409 nếu lời mời đã hết hạn hoặc chuyến đã có người nhận.
POST/v1/trips/{tripId}/eventsTài xế báo ARRIVED, PICKED_UP, COMPLETED. Server kiểm tra chuyển trạng thái hợp lệ.

Thiết kế tổng thể

Đi lần lượt từng yêu cầu chức năng. Mục tiêu ở đây là chạy đúng, việc tối ưu để dành cho phần đào sâu.

1) Tài xế gửi vị trí liên tục

Vị trí chỉ cần bản mới nhất để tìm xe, nên giữ trong bộ nhớ thay vì ghi DB mỗi lần. Lịch sử điểm GPS trong chuyến được đẩy sang hàng đợi để lưu sau, không nằm trên đường nóng.

  1. 1App tài xế gửi {lat, lng, ts} qua WebSocket tới Location gateway
  2. 2Gateway bỏ điểm cũ hơn điểm đã có (ts nhỏ hơn) rồi cập nhật Redis GEO, đặt TTL 30 s cho trạng thái online
  3. 3Nếu tài xế đang chạy chuyến, gateway đẩy điểm vào hàng đợi; worker gom theo chuyến và ghi xuống kho lưu trữ
  4. 4Trip service phát vị trí mới cho app khách đang theo dõi chuyến đó

Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (tài xế báo vị trí mỗi 4 s, bảng băm giữ vị trí mới nhất, QuadTree cập nhật thưa hơn) ; Chưa có nguồn trong thư viện: Redis GEO/GEOSEARCH và TTL 30 s trạng thái online không có trong sách

2) Khách xem xe gần và giá dự kiến

Truy vấn bán kính trên geo index trả danh sách tài xế trống. Giá được tính một lần và chốt thành quote có hạn để khách không bị đổi giá giữa chừng.

  1. 1App khách gọi GET /drivers/nearby với vị trí hiện tại
  2. 2Ride API chạy GEOSEARCH bán kính 2 km, lọc theo loại xe và trạng thái AVAILABLE
  3. 3Khách nhập điểm đến, POST /fare-quotes lấy ETA từ Maps API và hệ số surge của ô bản đồ
  4. 4Server lưu FareQuote với hạn 5 phút và trả quoteId

Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Workflow of our application" (tìm tài xế gần trong vùng) ; Grokking Modern System Design · ch.29 Design Yelp ↗ · mục "QuadTree" (tìm theo bán kính) ; Chưa có nguồn trong thư viện: Redis GEOSEARCH và Maps API

3) Đặt xe và ghép tài xế

Matching service mời từng tài xế một, theo thứ tự gần và phù hợp. Mỗi tài xế bị khoá trong lúc đang được mời để không ai khác mời trùng.

  1. 1Khách POST /trips kèm quoteId và Idempotency-Key, trip được tạo ở trạng thái MATCHING
  2. 2Matching lấy 10 tài xế gần nhất, thử khoá từng người bằng SET NX PX 15000
  3. 3Khoá được ai thì gửi lời mời cho người đó qua WebSocket, chờ tối đa 10 s
  4. 4Tài xế nhận: cập nhật trip sang ACCEPTED có điều kiện; từ chối hoặc hết giờ: nhả khoá, mời người tiếp theo

Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Workflow of our application" (matching chọn tài xế, gửi lời mời, tài xế nhận) ; Redis in Action ↗ · ch.6 · §6.2.5 Locks with timeouts, §6.2.3 Building a lock in Redis (SETNX) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages

4) Theo dõi chuyến và thanh toán

Trip service giữ máy trạng thái của chuyến và chỉ cho phép chuyển trạng thái hợp lệ. Thanh toán gắn idempotency key theo chuyến nên gọi lại bao nhiêu lần cũng chỉ thu một lần.

  1. 1Tài xế báo ARRIVED, PICKED_UP, COMPLETED; mỗi lần là một UPDATE có điều kiện trên trạng thái cũ
  2. 2Khi COMPLETED, Trip service ghi trip và một bản ghi outbox trong cùng transaction
  3. 3Payment service đọc sự kiện, gọi cổng thanh toán với key payment:{tripId}
  4. 4Kết quả từ cổng (kể cả webhook gửi lại) được chống trùng theo key rồi mới cập nhật Payment

Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (tr. 97–100) (ghi DB và phát sự kiện cùng transaction) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages ; Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Payment Service and Fraud Detection" ; Building Microservices ↗ (.zip) · ch.11 Microservices at Scale · mục "Idempotency" (tr. 215)

Hạ tầng theo quy mô

Cùng một sản phẩm, ba mức người dùng. Chọn từng mức để xem sơ đồ, phép tính và cấu hình server thay đổi thế nào.

Người dùng
≈ 10.000 khách/ngày · 1.000 tài xế online đỉnh
Tải đỉnh
≈ 290 req/s đỉnh (chủ yếu là vị trí)
Chi phí ước lượng
≈ 150–300 USD/tháng (chưa tính phí Maps API)

Một hãng taxi ở Hà Nội ra app riêng. Một API NestJS vừa phục vụ REST vừa giữ WebSocket, Redis GEO cho vị trí và khoá, Postgres cho chuyến. Hai instance là đủ, chạy 2 chỉ để dự phòng.

Người dùngApp kháchNgười dùngApp tài xếCân bằng tảiLoad balancerDịch vụAPI + WebSocketCacheRedis GEO + lockDatabasePostgresBên ngoàiMaps APIBên ngoàiFCM / APNsBên ngoàiCổng thanh toánHTTPSWS vị trí 4 sGEOSEARCHchuyếnETAthu tiền

Ước lượng

  • Vị trí: 1.000 tài xế online × 1 lần / 4 s = 250 cập nhật/s (đã là số online lúc đỉnh, không nhân thêm)
  • Request khác: 10.000 khách × 30 req / 86.400 ≈ 3,5 req/s, đỉnh ×10 ≈ 35 req/s
  • Chuyến: 8.000 chuyến/ngày / 86.400 ≈ 0,09 chuyến/s, giờ tan tầm ×10 ≈ 1 chuyến/s
  • API: (250 + 35 + 1) / (1.000 × 0,6) ≈ 0,48 → 1 instance, chạy 2 để dự phòng
  • Redis: 1.000 tài xế × (20 + 60 + 100) B × 1,3 ≈ 234 KB
  • Lộ trình GPS: 8.000 chuyến × 300 điểm (20 phút, 4 s/điểm) × 40 B ≈ 96 MB/ngày ≈ 35 GB/năm

Nguồn con số: Grokking Modern System Design · ch.30 Design Uber ↗ · bài "Requirements of Uber's Design" · mục "Resource estimation" (vị trí tài xế cập nhật 4 s, số tài xế, số chuyến) ; Grokking Modern System Design · ch.5 Back-of-the-envelope Calculations ↗ · mục "Requests estimation" ; Quy tắc ước lượng của khoá học · /hoc/chon-server (600 req/s mỗi instance, 60% CPU, Redis ×1,3, 100.000 ops/s mỗi node là giả định của khoá học)

Bảng hạ tầng

Thành phầnChọnSố lượngCấu hìnhVì sao
Load balancerCloud Load Balancing (hỗ trợ WebSocket)1managedChia tải cho 2 instance và giữ kết nối WebSocket lâu.
API + WebSocketNestJS trong Docker, chạy VM hoặc Cloud Run22 vCPU · 4 GB RAM286 req/s đỉnh chỉ cần 1 instance; 1.000 kết nối WebSocket rất nhẹ.
Geo index + khoáRedis (Memorystore) với GEOADD / GEOSEARCH, SET NX PX11 GB, bản Standard có replicaDữ liệu chưa tới 1 MB; 250 ghi/s không đáng kể với Redis.
DatabasePostgreSQL managed (Cloud SQL), bảng điểm GPS chia partition theo tháng1 + standby2 vCPU · 8 GB RAM · 100 GB SSDChuyến và thanh toán cần transaction; 35 GB/năm lộ trình vẫn vừa một node.
Bản đồ, ETAMaps API (Google Maps hoặc Goong)-trả theo lượt gọiTự làm routing không đáng ở quy mô này.
Thanh toánCổng thanh toán (MoMo, ZaloPay, VNPay, thẻ) + tiền mặt--Gọi qua một Payment module có idempotency key.
Thông báoFCM / APNs--Đánh thức app tài xế khi WebSocket bị hệ điều hành ngắt.
Thay đổi so với mức trước
  • Điểm xuất phát: một service, một Redis, một Postgres
  • Vị trí đã nằm trong Redis ngay từ đầu vì rẻ và tránh ghi DB 250 lần/s
  • Lộ trình GPS ghi theo lô vào Postgres, chưa cần hàng đợi
Nút thắt tiếp theo

Khi mở rộng ra cả Hà Nội và TP.HCM, cập nhật vị trí tăng lên hàng chục nghìn/s và chiếm hết CPU của API, làm chậm luồng đặt xe. Bảng điểm GPS trong Postgres cũng phình lên vài TB mỗi năm.

Cấu hình và chi phí là ước lượng để học và trình bày trong phỏng vấn. Xem cách tính ở bài Chọn server.

So sánh nhanh ba mức

Thành phầnMức 1Mức 2Mức 3
Người dùng≈ 10.000 khách/ngày · 1.000 tài xế online đỉnh≈ 1 triệu khách/ngày · 60.000 tài xế online đỉnh≈ 20 triệu khách/ngày · 1 triệu tài xế online đỉnh
Chi phí≈ 150–300 USD/tháng (chưa tính phí Maps API)≈ 4.000–8.000 USD/tháng (ước lượng, chưa tính phí Maps API)≈ 100.000–200.000 USD/tháng (ước lượng, chưa tính Maps và phí cổng thanh toán)
Load balancer
Cloud Load Balancing (hỗ trợ WebSocket)
1 × managed
Cloud Load Balancing, tách route /driver/stream và /v1/*
1 × managed
Chưa cần
API + WebSocket
NestJS trong Docker, chạy VM hoặc Cloud Run
2 × 2 vCPU · 4 GB RAM
Chưa cầnChưa cần
Geo index + khoá
Redis (Memorystore) với GEOADD / GEOSEARCH, SET NX PX
1 × 1 GB, bản Standard có replica
Chưa cầnChưa cần
Database
PostgreSQL managed (Cloud SQL), bảng điểm GPS chia partition theo tháng
1 + standby × 2 vCPU · 8 GB RAM · 100 GB SSD
PostgreSQL (Cloud SQL HA), bảng trips partition theo tháng
1 primary + 1 replica × 8 vCPU · 32 GB RAM · 1 TB SSD
Chưa cần
Bản đồ, ETA
Maps API (Google Maps hoặc Goong)
- × trả theo lượt gọi
Chưa cầnChưa cần
Thanh toán
Cổng thanh toán (MoMo, ZaloPay, VNPay, thẻ) + tiền mặt
- × -
Chưa cầnChưa cần
Thông báo
FCM / APNs
- × -
Chưa cầnChưa cần
Location gatewayChưa cần
NestJS (ws) trên GKE, autoscale theo CPU
25 × 2 vCPU · 4 GB RAM
Pod trên Kubernetes (GKE), HPA theo số message
417 pod × 2 vCPU · 4 GB RAM
Ride APIChưa cần
NestJS, stateless, autoscale 3–10
3 × 2 vCPU · 4 GB RAM
Chưa cần
MatchingChưa cần
Service riêng, đọc Redis GEO, khoá tài xế bằng SET NX PX
3 × 2 vCPU · 4 GB RAM
Service partition theo ô H3, mỗi ô do một consumer xử lý
20 pod × 2 vCPU · 4 GB RAM
Trip + PaymentChưa cần
NestJS, máy trạng thái chuyến, outbox, gọi cổng thanh toán
2 × 2 vCPU · 4 GB RAM
Chưa cần
RedisChưa cần
Memorystore, một instance cho mỗi thành phố (HN, HCM)
2 primary + 2 replica × 5 GB mỗi instance
Chưa cần
Hàng đợiChưa cần
GCP Pub/Sub, topic location-points và trip-events
- × managed
Chưa cần
Lưu lộ trìnhChưa cần
Worker gom điểm theo chuyến, ghi file lên GCS
2 worker × 2 vCPU · 4 GB RAM
Chưa cần
EdgeChưa cầnChưa cần
Global LB, định tuyến theo quốc gia về region gần nhất (Singapore, Jakarta)
2 region × managed
Ride + Trip APIChưa cầnChưa cần
NestJS trên GKE, stateless
58 pod × 2 vCPU · 4 GB RAM
Geo indexChưa cầnChưa cần
Redis Cluster, key theo ô H3 độ phân giải 9
9 primary + 9 replica × 4 vCPU · 8 GB RAM
Database chuyếnChưa cầnChưa cần
PostgreSQL shard theo city_id (Citus) hoặc Cloud Spanner
6 shard × (1 primary + 1 replica) × 8 vCPU · 32 GB RAM
Event busChưa cầnChưa cần
Kafka, topic location, trip-events, payments; key theo ô hoặc tripId
6 broker × 8 vCPU · 32 GB RAM · NVMe
Surge + đặc trưng ETAChưa cầnChưa cần
Flink đọc Kafka, đếm cung cầu theo ô H3 mỗi phút, ghi hệ số vào Redis
8 TaskManager × 4 vCPU · 16 GB RAM
Lưu trữ lịch sửChưa cầnChưa cần
Cassandra cho chuyến đã xong + GCS/data lake cho lộ trình GPS
6 node Cassandra × 8 vCPU · 32 GB · 4 TB

Đào sâu

Mỗi câu hỏi đi từ cách đơn giản đến cách người phỏng vấn mong đợi ở cấp senior.

1) Tài xế gửi vị trí 4 giây một lần. Lưu và đánh chỉ mục vị trí thế nào để vừa chịu ghi vừa tìm xe nhanh?

Cách chưa tốt
UPDATE bảng drivers trong Postgres mỗi lần nhận vị trí, tìm bằng lat BETWEEN và lng BETWEEN

Mỗi UPDATE sinh một bản ghi mới trong Postgres (MVCC), 15.000 lần/s tạo rất nhiều dead tuple và WAL. Index B-tree trên lat, lng riêng rẽ chỉ lọc được một chiều, chiều còn lại phải quét.

Đánh đổi: Đơn giản nhưng DB chính bị vắt kiệt bởi dữ liệu vốn chỉ cần sống vài giây.

Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (vì sao không cập nhật QuadTree/DB mỗi 4 s: tốn thời gian sửa cấu trúc) ; Chưa có nguồn trong thư viện: cơ chế MVCC, dead tuple và WAL của Postgres không có trong sách

Cách tốt
Postgres + PostGIS với index GiST

PostGIS có kiểu geography và ST_DWithin, index GiST tìm bán kính đúng nghĩa. Ở mức vài trăm cập nhật/s như mức 1 thì chạy tốt và chỉ cần một DB.

Đánh đổi: Tốc độ ghi vẫn giới hạn bởi một primary; lên hàng chục nghìn cập nhật/s thì vacuum và WAL thành vấn đề.

Nguồn: Chưa có nguồn trong thư viện: PostGIS và index GiST không có trong sách ; Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (giải pháp QuadTree)

Cách tốt
Redis GEO (GEOADD, GEOSEARCH) giữ vị trí mới nhất trong RAM

Redis GEO là sorted set với điểm là geohash 52 bit, nên GEOADD và tìm bán kính đều nhanh. Một node chịu khoảng 100.000 ops/s, đủ cho 15.000 cập nhật/s. Chia key theo thành phố (geo:hn, geo:hcm) để mỗi set nhỏ và dễ tách node.

Đánh đổi: Mất node là mất vị trí, nhưng chấp nhận được vì tài xế gửi lại sau 4 s. Một key thành phố lớn vẫn là hot key.

Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (bảng băm cập nhật mỗi 4 s, QuadTree làm mới mỗi 10-15 s) ; Chưa có nguồn trong thư viện: Redis GEO (geohash 52 bit) và key theo thành phố không có trong sách

Cách tốt nhất
Chia bản đồ thành ô (geohash hoặc H3), index trong bộ nhớ theo ô, shard theo ô

Mỗi tài xế thuộc một ô H3 (lục giác, khoảng 0,1 km² ở độ phân giải 9). Tìm xe gần là đọc ô hiện tại và vòng ô lân cận, nên chi phí cố định. Ô là đơn vị shard tự nhiên, cũng là đơn vị tính surge. Grokking dùng QuadTree cho Yelp, nhưng với điểm di chuyển liên tục thì sửa cây tốn kém, nên sách đề xuất bảng hash cập nhật 4 s và chỉ làm mới QuadTree mỗi 10–15 s.

Đánh đổi: Phải tự xử lý tài xế ở mép ô và đổi ô; H3 lục giác cho khoảng cách đều hơn geohash nhưng cần thêm thư viện.

Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Design" (bảng băm vị trí 4 s + QuadTree làm mới 10-15 s, tr. mục "Dynamic segment solution") ; Grokking Modern System Design · ch.29 Design Yelp ↗ · mục "QuadTree" (chia bản đồ thành ô) ; Chưa có nguồn trong thư viện: H3/geohash không có trong sách

2) Làm sao ghép khách với tài xế mà không giao một tài xế cho hai chuyến cùng lúc?

Cách chưa tốt
Gửi lời mời cho mọi tài xế gần, ai bấm trước thì được

Nhiều chuyến cùng mời một tài xế, tài xế bấm nhận ở hai màn hình gần như cùng lúc. Không có chỗ nào đảm bảo chỉ một lần nhận thắng.

Đánh đổi: Ghép nhanh nhưng sinh tranh chấp, tài xế bị spam lời mời.

Nguồn: Microservices Patterns ↗ (.zip) · ch.4 · §4.1.3 Using the Saga pattern to maintain data consistency (vấn đề thiếu cô lập khi nhiều giao dịch chạm cùng dữ liệu)

Cách tốt
UPDATE có điều kiện trong DB khi tài xế nhận

UPDATE drivers SET status='ON_TRIP', trip_id=? WHERE id=? AND status='AVAILABLE'. Chỉ một transaction cập nhật được 1 dòng, các lần khác nhận 0 dòng và trả 409.

Đánh đổi: Đúng tuyệt đối nhưng mọi lần mời đều phải chạm DB, và vẫn có thể mời trùng rồi mới từ chối ở bước nhận.

Nguồn: Microservices Patterns ↗ (.zip) · ch.4 · §4.1.3 Using the Saga pattern to maintain data consistency (đảm bảo một lần cập nhật thắng qua điều kiện trạng thái) ; Chưa có nguồn trong thư viện: UPDATE ... WHERE status là kỹ thuật SQL cơ bản

Cách tốt nhất
Khoá tài xế bằng Redis có TTL lúc mời, xác nhận bằng UPDATE có điều kiện lúc nhận

Trước khi mời, matching chạy SET lock:driver:{id} {tripId} NX PX 15000. Không lấy được khoá thì bỏ qua tài xế đó. Khoá tự hết hạn nếu matching chết giữa chừng. Khi tài xế nhận, trip chuyển MATCHING sang ACCEPTED bằng UPDATE ... WHERE status='MATCHING', đây mới là nguồn sự thật. Ở quy mô lớn, mỗi ô bản đồ do một matcher xử lý (partition theo ô) nên tranh chấp gần như biến mất.

Đánh đổi: Redis lock không tuyệt đối khi failover, nên bước cuối vẫn phải là điều kiện trong DB; lời mời tuần tự làm khách chờ lâu hơn khi nhiều tài xế từ chối.

Nguồn: Redis in Action ↗ · ch.6 · §6.2.3 Building a lock in Redis, §6.2.5 Locks with timeouts (khoá có hạn tự hết) ; Chưa có nguồn trong thư viện: mỗi matcher xử lý một ô bản đồ là thiết kế của khoá học

3) Tính ETA và giá surge thế nào cho đủ nhanh và đủ đúng?

Cách chưa tốt
Khoảng cách đường chim bay chia tốc độ trung bình

Ở Hà Nội, hai điểm cách nhau 1 km theo đường thẳng có thể phải đi 4 km vì đường một chiều và sông. ETA sai thì khách huỷ và giá cũng sai theo.

Đánh đổi: Rẻ, nhanh, nhưng sai nhiều nhất đúng ở những chỗ khách để ý.

⚠ Chưa có nguồn trong thư viện: ETA theo đường chim bay không được bàn trong sách; ví dụ Hà Nội là giả định của khoá học

Cách tốt
Gọi Maps API (Google, Goong) cho từng báo giá, cache theo cặp ô

Dùng routing có dữ liệu giao thông. Cache kết quả theo cặp ô H3 đi và đến trong 2–5 phút để giảm số lần gọi.

Đánh đổi: Phí API tăng theo số báo giá, thường là khoản lớn nhất trong hoá đơn hạ tầng ở mức 2.

Nguồn: Grokking Modern System Design · ch.30 Design Uber ↗ · mục "Workflow of our application" (ETA/ngoại lai) ; Chưa có nguồn trong thư viện: Maps API (Google, Goong) và cache theo cặp ô

Cách tốt nhất
Routing engine riêng trên đồ thị đã tiền xử lý + model hiệu chỉnh, surge tính theo ô mỗi phút

Đường được tìm trên đồ thị chia vùng với contraction hierarchies, trọng số cạnh là tốc độ thực tế lấy từ GPS tài xế. Một model hiệu chỉnh sai số theo giờ, thời tiết, khu vực. Surge: stream job đếm yêu cầu và tài xế trống trên mỗi ô H3 mỗi phút, hệ số làm mượt và có trần, sau đó chốt vào quote.

Đánh đổi: Tốn công xây và vận hành, chỉ đáng khi số báo giá đủ lớn để phí Maps API vượt chi phí đội ngũ.

⚠ Chưa có nguồn trong thư viện: contraction hierarchies, model hiệu chỉnh ETA và surge pricing không có trong sách; chỉ nêu ý tưởng

4) Quản lý trạng thái chuyến và thanh toán sao cho mạng chập chờn không gây sai trạng thái hay trừ tiền hai lần?

Cách chưa tốt
App gửi trạng thái mới, server ghi đè thẳng

Request COMPLETED gửi lại đến sau CANCELLED có thể đưa chuyến về trạng thái sai. App retry lệnh thanh toán thì cổng thanh toán thu hai lần.

Đánh đổi: Ít code nhưng sai dữ liệu tiền, loại lỗi đắt nhất.

Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages ; Building Microservices ↗ (.zip) · ch.11 · mục "Idempotency" (tr. 215)

Cách tốt
Máy trạng thái phía server, chuyển trạng thái bằng UPDATE có điều kiện

Bảng chuyển hợp lệ: ACCEPTED → ARRIVED → IN_PROGRESS → COMPLETED. Mỗi lệnh là UPDATE ... WHERE id=? AND status=<trạng thái trước>. Request trùng hoặc đến muộn cập nhật 0 dòng và được trả kết quả hiện tại.

Đánh đổi: Trạng thái đúng, nhưng chưa xử lý trường hợp đã lưu COMPLETED mà gửi lệnh thanh toán thất bại.

Nguồn: Microservices Patterns ↗ (.zip) · ch.4 · §4.1.3 Using the Saga pattern to maintain data consistency (trạng thái từng bước) ; Chưa có nguồn trong thư viện: máy trạng thái bằng UPDATE có điều kiện

Cách tốt nhất
Máy trạng thái + outbox + idempotency key ở mọi bước thanh toán

COMPLETED và sự kiện TripCompleted ghi chung transaction (outbox). Payment service tiêu thụ sự kiện theo kiểu idempotent, gọi cổng thanh toán với key payment:{tripId}, chống trùng cả webhook trả về. Thẻ được giữ tiền (authorize) lúc đặt và thu (capture) lúc kết thúc. Sổ cái ghi kép giúp đối soát. Tiền mặt vẫn đi qua cùng sổ cái để tính hoa hồng tài xế.

Đánh đổi: Nhiều thành phần hơn và thanh toán là bất đồng bộ, khách có thể thấy biên lai sau vài giây.

Nguồn: Microservices Patterns ↗ (.zip) · ch.3 · §3.3.7 Transactional messaging (outbox, tr. 97–100) ; Microservices Patterns ↗ (.zip) · ch.3 · §3.3.6 Handling duplicate messages ; Building Microservices ↗ (.zip) · ch.11 · mục "Idempotency" ; Chưa có nguồn trong thư viện: sổ cái ghi kép, authorize/capture của cổng thanh toán

Trong phỏng vấn

Nên nói
  • “Vị trí tài xế là dữ liệu sống vài giây, nên tôi giữ trong bộ nhớ và chấp nhận mất khi node chết, tài xế sẽ gửi lại sau 4 giây.”
  • “Tôi tách hai luồng có tải khác hẳn nhau: cập nhật vị trí (hàng chục nghìn/s, chịu mất) và đặt xe (ít hơn nhiều, không được sai).”
  • “Khoá Redis có TTL để tránh mời trùng, nhưng nguồn sự thật cuối cùng là UPDATE có điều kiện trên trạng thái chuyến trong DB.”
  • “Thanh toán đi qua outbox và idempotency key theo tripId, nên retry hay webhook trùng cũng chỉ thu một lần.”
Bẫy hay gặp
  • Ghi mọi cập nhật vị trí vào DB quan hệ rồi mới nghĩ tới cache
  • Gửi lời mời cho nhiều tài xế cùng lúc mà không nói cách chặn hai người cùng nhận
  • Dùng QuadTree cho điểm di chuyển liên tục mà không nói chi phí cập nhật cây
  • Quên tiền mặt và quên trường hợp app tài xế mất sóng giữa chuyến

Đọc thêm

Sách trong thư viện của bạn. Số chương có thể lệch nhẹ so với bản PDF.