Case Study: Design Nearby Friends (Real-Time Location Sharing)

” tưởng tượng bạn đang đi chơi với nhóm bạn ở Sài Gòn. Ai cũng trên Zalo, nhưng không ai biết ai đang ở đâu. Bạn mở app — và trong vòng 1 giây, bản đồ hiện ra 5 người bạn đang ở gần bạn trong bán kính 5km. Phía sau sự ‘đơn giản’ đó là một hệ thống real-time phức tạp, nơi WebSocket gặp Pub/Sub gặp Redis ở quy mô 100 triệu người dùng.”

Tags: system-design nearby-friends real-time websocket pubsub redis alex-xu case-study vol2 Student: Hieu Prerequisite: Tuan-02-Back-of-the-envelope · Tuan-06-Cache-Strategy · Tuan-10-Consistent-Hashing · Tuan-17-Design-Chat-System Lien quan: Case-Design-Proximity-Service · Tuan-05-Load-Balancer · Tuan-08-Message-Queue · Tuan-13-Monitoring-Observability · Tuan-14-AuthN-AuthZ-Security Reference: Alex Xu, System Design Interview — An Insider’s Guide, Volume 2 — Chapter 2: Nearby Friends


1. Context & Why — Tại sao Nearby Friends quan trọng?

1.1 Analogy — Nhóm bạn đi chơi trong thành phố

hãy tưởng tượng bạn và 10 người bạn cùng đi chơi ở TP.HCM vào tối thứ 7. Mỗi người có kế hoạch riêng — có người ở quận 1 ăn uống, có người ở Bitexco mua sắm, có người ở Thủ Đức. Bạn muốn biết: ai đang ở gần mình để rủ nhau?

Cách truyền thống: Bạn nhắn tin hỏi từng người — “Ey, mày đang ở đâu?” — rồi đợi từng người trả lời. 10 người, 10 tin nhắn, 10 lần đợi. Có người không trả lời, có người trả lời trễ 30 phút. Lúc đó họ đã đi chỗ khác rồi.

Cách Nearby Friends: Bạn mở app, bật tính năng “Nearby Friends”. App tự động hiển thị trên bản đồ: “Minh đang ở Nguyễn Huệ, Tuấn đang ở Đồng Khởi cách 500m, Lan đang ở Bùi Viện cách 800m.” Cập nhật mỗi 30 giây. Bạn không cần hỏi ai cả — hệ thống làm mọi thứ tự động.

Đây chính là tính năng Nearby Friends — tương tự Snap Map (Snapchat), Zalo Nearby, Facebook Nearby Friends (đã discontinued 2022), hoặc WhatsApp Live Location.

1.2 Vấn đề kỹ thuật — Tại sao bài toán này khó?

Vấn đềGiải thíchQuy mô
Real-time location updatesVị trí thay đổi liên tục, mỗi 30 giây phải cập nhật. Không phải query 1 lần như Proximity Service10M concurrent users x update mỗi 30s = ~333K updates/s
Bidirectional communicationServer cần push location của bạn bè đến client, không chỉ client pullHTTP polling lãng phí, cần WebSocket
Fan-out problemMỗi user có trung bình 400 bạn. Mỗi update location phải notify tới 400 người333K updates/s x 400 friends = 133M messages/s
Stateful connectionsWebSocket là stateful — khó scale hơn stateless HTTPCần connection management, reconnection, server affinity
Privacy sensitiveLocation là personal data nhạy cảm nhất — cần opt-in, opt-out, precision controlGDPR, CCPA, luật bảo vệ dữ liệu cá nhân
Selective sharingChỉ hiển thị bạn bè đã opt-in và đang online. Không phải tất cả 400 bạn bèFiltering logic phức tạp

1.3 So sánh với Proximity Service

Khía cạnhProximity Service (Chapter 1)Nearby Friends (Chapter 2)
Đối tượngBusinesses (tĩnh)Friends (động — di chuyển liên tục)
Update frequencyBusinesses hiếm khi đổi vị tríUsers di chuyển mỗi giây
CommunicationRequest-Response (HTTP)Bidirectional (WebSocket)
Data freshnessChấp nhận stale vài giờCần real-time (< 30s)
IndexingGeohash/Quadtree cho millions of POIsKhông cần geospatial index — chỉ check khoảng cách giữa friends
Scale challengeRead-heavy (60K QPS)Write-heavy + fan-out (133M messages/s)
ReferenceCase-Design-Proximity-ServiceBài này

Insight quan trọng: Nearby Friends KHÔNG phải là Proximity Service cho người dùng. Proximity Service tìm “ai ở gần mình?” trong tất cả người lạ. Nearby Friends chỉ hiển thị bạn bè — bạn đã biết danh sách bạn bè, chỉ cần biết vị trí hiện tại của họ.

1.4 Real-World Applications

AppTính năngĐặc điểm
Snap Map (Snapchat)Hiển thị vị trí bạn bè trên bản đồReal-time, Bitmoji avatar, Ghost Mode để ẩn
Zalo NearbyTìm người dùng Zalo gần vị trí hiện tạiKhác — tìm người lạ, không chỉ bạn bè
WhatsApp Live LocationChia sẻ vị trí real-time với contact/groupCó thời hạn (15min, 1h, 8h)
Find My Friends (Apple)Xem vị trí gia đình/bạn bèTích hợp iOS, battery-efficient
Telegram Live LocationChia sẻ vị trí trong chatTương tự WhatsApp

2. Step 1 — Understand the Problem & Establish Design Scope

2.1 Clarifying Questions

Câu hỏiTrả lờiGhi chú
Tính năng chính là gì?Hiển thị danh sách bạn bè đang ở gần mình, trên bản đồ, cập nhật real-timeCore feature duy nhất
”Gần” nghĩa là bao xa?Trong bán kính có thể cấu hình, mặc định 5 miles (~8 km)Configurable radius
Tần suất cập nhật?Mỗi 30 giâyKhông phải real-time từng giây — 30s là đủ
Quy mô bao lớn?100M DAU (Daily Active Users)Scale của Facebook/Zalo
Bao nhiêu người online đồng thời?~10% của DAU ở peak = 10M concurrentPeak hours: 7-10 PM
Trung bình mỗi user có bao nhiêu bạn?400 friendsTrung bình Facebook ~338, làm tròn 400
User cần opt-in không?Có — chỉ user bật tính năng mới chia sẻ vị tríPrivacy là số 1
Có cần lưu location history?Không — chỉ cần vị trí hiện tạiGiảm storage, tăng privacy
Hiển thị khoảng cách hay vị trí chính xác?Cả hai — khoảng cách + vị trí trên bản đồClient render

2.2 Functional Requirements

  • FR1: User có thể bật/tắt tính năng Nearby Friends (opt-in/opt-out)
  • FR2: Khi bật, app hiển thị danh sách bạn bè đang ở trong bán kính configurable (mặc định 5 miles)
  • FR3: Danh sách cập nhật mỗi 30 giây mà không cần user refresh
  • FR4: Mỗi friend entry hiển thị: tên, khoảng cách, thời gian cập nhật cuối cùng
  • FR5: Chỉ hiển thị bạn bè cùng bật tính năng (mutual opt-in)
  • FR6: Khi user tắt tính năng hoặc offline, biến mất khỏi danh sách của bạn bè

2.3 Non-Functional Requirements

Yêu cầuMục tiêuGiải thích
Availability99.9%Tính năng social, không phải safety-critical như navigation
LatencyLocation update propagation < 1 giâyTừ lúc friend update location đến lúc bạn nhận được
Scalability10M concurrent users, 333K location updates/sPeak traffic
ConsistencyEventual consistency okNhận vị trí trễ 1-2 giây là chấp nhận được
Battery efficiencyKhông drain battery quá nhiềuGPS polling mỗi 30s, không phải liên tục
PrivacyLocation chỉ chia sẻ với friends đã opt-inGDPR compliant

2.4 Estimation — Back-of-the-Envelope

Concurrent Users:

Location Update QPS:

Mỗi giây, hệ thống nhận ~333K location updates từ clients. Đây là write-heavy.

WebSocket Connections:

Nếu mỗi WebSocket server handle 50K connections (con số thực tế cho production):

Pub/Sub Fan-out Volume:

Nhưng không phải tất cả 400 friends đều online và opt-in. Giả sử 10% friends đang online và opt-in:

Đây là con số quan trọng: 13.3 triệu messages mỗi giây cần được deliver qua Pub/Sub system. Đây là thách thức lớn nhất của bài toán.

Redis Memory cho Location Cache:

Chỉ 1 GB Redis memory cho location cache của 10M users. Một Redis instance (64 GB RAM) dư sức.

Bandwidth cho Location Updates:

Outbound bandwidth gấp 40x inbound — đặc trưng của fan-out system.

Tóm tắt Estimation:

MetricValue
Concurrent users (peak)10M
WebSocket servers (50K conn/server)200
Location update QPS~333K/s
Pub/Sub fan-out messages~13.3M/s
Redis memory (location cache)~1 GB
Inbound bandwidth~33 MB/s
Outbound bandwidth~1.33 GB/s

3. Step 2 — High-Level Design

3.1 Lựa chọn Communication Protocol

Trước khi thiết kế architecture, phải chọn giao thức giao tiếp. Đây là quyết định quan trọng nhất của bài toán.

OptionMô tảƯu điểmNhược điểmPhù hợp?
HTTP PollingClient gửi GET request mỗi 30sĐơn giản, statelessLãng phí bandwidth (mỗi request có HTTP headers ~500 bytes), 10M requests mỗi 30s = 333K QPS chỉ để pollKhông
HTTP Long PollingClient gửi request, server giữ cho đến khi có data mớiÍt lãng phí hơn pollingVẫn là 1 connection per pending request, không thật sự bidirectionalKhông
Server-Sent Events (SSE)Server push events qua HTTPĐơn giản, built-in browser supportChỉ server → client, không có client → server. Mà ta cần cả hai chiềuKhông đủ
WebSocketFull-duplex, bidirectional communication trên 1 TCP connectionClient gửi location, server push friend updates — trên cùng 1 connection. Lightweight (2-6 bytes overhead/message)Stateful, khó scale hơn HTTPCó — đây là lựa chọn

Tại sao WebSocket? Vì ta cần bidirectional communication: client gửi location update (client → server) và server push friend location (server → client) trên cùng 1 connection. WebSocket là lựa chọn tự nhiên nhất. Tham chiếu Tuan-17-Design-Chat-System — chat system cũng dùng WebSocket vì lý do tương tự.

3.2 High-Level Architecture Overview

Hệ thống gồm 3 thành phần chính:

ComponentChức năngTechnology
WebSocket ServersDuy trì persistent connections với clients, nhận location updates, push friend locationsWebSocket (ws/wss)
Location CacheLưu vị trí hiện tại của mỗi user (latest location)Redis
Pub/SubPropagate location updates đến tất cả friends đang onlineRedis Pub/Sub

Data Flow tóm tắt:

  1. User A bật tính năng → client mở WebSocket connection đến server
  2. Client gửi location update mỗi 30 giây qua WebSocket
  3. Server lưu location vào Redis (Location Cache)
  4. Server publish location update lên Pub/Sub channel của User A
  5. Tất cả friends của User A đang subscribe channel này → nhận được update
  6. Friends’ WebSocket servers tính khoảng cách → push đến friend’s client nếu trong radius

3.3 High-Level Architecture Diagram

flowchart TB
    subgraph Clients
        A["User A<br/>(Mobile App)"]
        B["User B<br/>(Mobile App)"]
        C["User C<br/>(Mobile App)"]
    end

    subgraph "API Gateway / Load Balancer"
        LB["Load Balancer<br/>→ [[Tuan-05-Load-Balancer]]"]
    end

    subgraph "WebSocket Server Fleet"
        WS1["WebSocket Server 1<br/>50K connections"]
        WS2["WebSocket Server 2<br/>50K connections"]
        WSN["WebSocket Server N<br/>50K connections"]
    end

    subgraph "Data Layer"
        Redis_Cache["Redis — Location Cache<br/>Key: user_id<br/>Value: {lat, lng, timestamp}"]
        Redis_PubSub["Redis Pub/Sub<br/>Channel per user<br/>Friends subscribe"]
    end

    subgraph "Supporting Services"
        UserSvc["User Service<br/>(Friends list, profile)"]
        DB[("Database<br/>User data, friend relationships")]
    end

    A -->|"WebSocket"| LB
    B -->|"WebSocket"| LB
    C -->|"WebSocket"| LB

    LB --> WS1
    LB --> WS2
    LB --> WSN

    WS1 & WS2 & WSN -->|"SET location"| Redis_Cache
    WS1 & WS2 & WSN -->|"PUBLISH update"| Redis_PubSub
    WS1 & WS2 & WSN -->|"SUBSCRIBE friend channels"| Redis_PubSub

    WS1 & WS2 & WSN -->|"Get friends list"| UserSvc
    UserSvc --> DB

    style LB fill:#42a5f5,color:#fff
    style Redis_Cache fill:#ef5350,color:#fff
    style Redis_PubSub fill:#ff7043,color:#fff
    style DB fill:#66bb6a,color:#fff

3.4 API Design (WebSocket Messages)

Vì dùng WebSocket, không có REST endpoints truyền thống. Thay vào đó, là các message types:

Client → Server Messages:

Message TypePayloadMục đích
location_update{lat, lng, timestamp}Gửi vị trí hiện tại mỗi 30s
enable_nearby{}Bật tính năng Nearby Friends
disable_nearby{}Tắt tính năng Nearby Friends
update_radius{radius_miles}Thay đổi bán kính hiển thị

Server → Client Messages:

Message TypePayloadMục đích
friend_location{friend_id, lat, lng, timestamp, distance}Vị trí mới của một friend
friend_offline{friend_id}Friend vừa tắt tính năng hoặc offline
nearby_friends_list[{friend_id, lat, lng, distance}, ...]Danh sách đầy đủ khi mới connect

4. Step 3 — Deep Dive

4.1 Location Update Flow — Chi tiết từng bước

Đây là flow quan trọng nhất của hệ thống. Khi User A gửi location update, chuyện gì xảy ra?

sequenceDiagram
    participant A as User A (Client)
    participant WS_A as WebSocket Server<br/>(serving User A)
    participant Cache as Redis<br/>Location Cache
    participant PubSub as Redis<br/>Pub/Sub
    participant WS_B as WebSocket Server<br/>(serving User B)
    participant B as User B (Client)

    Note over A,B: User A và User B là bạn bè. Cả hai đều bật Nearby Friends.

    A->>WS_A: location_update {lat: 10.77, lng: 106.70, ts: ...}

    par Parallel Operations
        WS_A->>Cache: SET user:A:location {lat, lng, ts}<br/>TTL = 120s
    and
        WS_A->>PubSub: PUBLISH channel:user_A {lat, lng, ts}
    end

    Note over PubSub: User B đã SUBSCRIBE channel:user_A<br/>(vì B là bạn của A và đang online)

    PubSub->>WS_B: Message on channel:user_A {lat, lng, ts}

    Note over WS_B: WS_B tính khoảng cách giữa A và B.<br/>Nếu <= radius của B → push đến B.

    WS_B->>B: friend_location {friend_id: A, lat, lng, distance: 1.2km}

Chi tiết từng bước:

BướcActionComponentLatencyGhi chú
1Client gửi location qua WebSocketClient → WS Server~5ms (LAN)Binary message, minimal overhead
2aLưu location vào Redis cacheWS Server → Redis~1msSET với TTL 120s (2x update interval)
2bPublish lên user’s Pub/Sub channelWS Server → Redis Pub/Sub~1msSong song với bước 2a
3Redis fan-out đến subscribersRedis Pub/Sub → WS Servers~1msMỗi subscriber nhận được message
4Tính khoảng cáchWS Server (receiver)~0.01msHaversine formula, CPU-bound
5Push đến client nếu trong radiusWS Server → Client~5msQua WebSocket connection
Total~10-15msCực nhanh

Aha Moment: Toàn bộ flow từ A update location đến B nhận được chỉ mất ~10-15ms. User cảm nhận là “real-time” mặc dù thực tế location chỉ cập nhật mỗi 30 giây. Bottleneck không phải latency — mà là fan-out volume.

4.2 Redis Pub/Sub — Trái tim của hệ thống

4.2.1 Thiết kế Channel

Core design decision: Mỗi user = 1 Pub/Sub channel.

Thiết kếMô tảƯu điểmNhược điểm
1 channel per user (chosen)Channel user:A, friends subscribeĐơn giản, granular controlNhiều channels (10M)
1 channel per geohash cellChannel geo:w3gvk, users trong cell subscribeÍt channelsNhận updates từ người lạ (không chỉ friends), privacy issue
1 global channelMỗi update broadcast đến tất cảĐơn giản nhất333K updates/s x 10M subscribers = không khả thi

Tại sao 1 channel per user? Vì Nearby Friends chỉ care về bạn bè, không phải tất cả người trong khu vực. Mỗi user chỉ cần subscribe channels của bạn bè — chính xác những người họ quan tâm.

4.2.2 Subscribe/Unsubscribe Flow

Khi User B lên online và bật Nearby Friends:

BướcActionChi tiết
1B connect WebSocketMở persistent connection
2Server lấy friends list của BQuery User Service → DB. Kết quả: [A, C, D, E, …] (400 friends)
3Server check ai đang online và opt-inQuery Redis: EXISTS user:A:location, user:C:location, …
4Server subscribe B vào channels của online friendsSUBSCRIBE channel:user_A, channel:user_C, …
5Server lấy latest location của online friendsMGET user:A:location, user:C:location, … từ Redis cache
6Server tính khoảng cách và gửi initial listPush nearby_friends_list đến B qua WebSocket

Khi User B offline hoặc tắt Nearby Friends:

BướcActionChi tiết
1Server UNSUBSCRIBE B khỏi tất cả friend channelsUNSUBSCRIBE channel:user_A, channel:user_C, …
2Server DELETE B’s location từ cacheDEL user:B:location
3Server PUBLISH “offline” event lên B’s channelPUBLISH channel:user_B {status: “offline”}
4Friends của B nhận “offline” eventPush friend_offline {friend_id: B} đến friends’ clients

4.2.3 Pub/Sub Fan-out Analysis

Đây là phần phức tạp nhất. Khi User A update location:

Nhưng không phải mỗi user có đúng 400 friends. Phân phối thực tế là long-tail:

User typeSố friends% usersSubscribers per channel
Light users< 10040%~10
Average users100-50045%~40
Heavy users500-200014%~100-200
Power users / celebrities2000-50001%~500+

Pitfall: Fan-out explosion với popular users. Một user có 5000 friends và 500 người đang online → mỗi 30 giây, 1 update của họ fan-out thành 500 messages. Nhân với 333K updates/s → có thể có những burst cực lớn.

Giải pháp cho popular users: Rate limit fan-out. Nếu user có > 500 subscribers, giảm tần suất update từ 30s xuống 60s hoặc 120s. User bình thường không bị ảnh hưởng.

4.2.4 Memory của Redis Pub/Sub Channels

7 GB cho Pub/Sub metadata — chấp nhận được cho một Redis cluster.

4.3 WebSocket Connection Management

4.3.1 Connection Lifecycle

stateDiagram-v2
    [*] --> Connecting: User mở app,<br/>bật Nearby Friends
    Connecting --> Connected: WebSocket handshake OK
    Connected --> Subscribing: Server subscribe<br/>friend channels
    Subscribing --> Active: Subscriptions done,<br/>initial list sent
    Active --> Active: Location updates<br/>mỗi 30s
    Active --> Reconnecting: Network drop,<br/>server crash
    Reconnecting --> Connecting: Retry với<br/>exponential backoff
    Active --> Disconnecting: User tắt tính năng<br/>hoặc close app
    Disconnecting --> [*]: Cleanup subscriptions,<br/>delete location cache
    Reconnecting --> [*]: Max retries exceeded

4.3.2 Connection Parameters

ParameterValueLý do
Heartbeat interval30 giâyTrùng với location update interval — gửi heartbeat kèm location
Connection timeout10 giâyNếu không connect được trong 10s → retry
Max reconnect attempts10Sau 10 lần → thông báo user “Không thể kết nối”
Reconnect backoffExponential: 1s, 2s, 4s, 8s, …, max 60sTránh thundering herd khi server restart
Idle timeout5 phút không nhận location updateCoi như user đã tắt GPS hoặc app bị kill → disconnect, cleanup
Max message size1 KBLocation update chỉ cần ~100 bytes. 1 KB là đủ margin

4.3.3 Reconnection Strategy

Khi WebSocket connection bị mất (network issue, server crash, app bị background), client cần reconnect:

BướcActionChi tiết
1Client detect disconnectonclose hoặc onerror event
2Wait theo backoffExponential: 1s, 2s, 4s, 8s, 16s, 32s, 60s (cap)
3Reconnect đến Load BalancerCó thể connect đến server khác (stateful concern!)
4Re-authenticateGửi token qua WebSocket
5Server re-subscribe friend channelsGiống flow ban đầu
6Server gửi initial nearby friends listClient refresh UI

Quan trọng: Khi reconnect, client có thể được route đến server khác (vì Load Balancer). Server mới phải re-subscribe tất cả friend channels. Đây là chi phí của reconnection — nhưng vì subscribe là O(number_of_friends) và friends list được cache, chi phí này chấp nhận được.

4.4 Scaling WebSocket Servers

4.4.1 Thách thức — Stateful Connections

WebSocket connections là stateful: mỗi connection gắn với 1 server cụ thể. Không thể “load balance từng request” như HTTP. Sau khi connection được thiết lập, mọi message phải đi qua đúng server đó.

Vấn đềGiải thíchGiải pháp
Server failureServer chết → 50K users mất connectionClient auto-reconnect, LB route đến server khác
Uneven distributionServer A có 60K connections, server B có 20KLB track connection count, route new connections đến server ít tải
DeploymentRolling update → server restart → 50K users reconnect đồng thờiGraceful shutdown: server thông báo clients trước 30s, clients reconnect dần
Memory pressureMỗi connection ~ 10-50 KB memory. 50K connections = 0.5-2.5 GB/serverMonitor memory, set max connections per server

4.4.2 Server Assignment Strategy

StrategyMô tảƯu điểmNhược điểm
Random (via LB)LB random chọn server cho new connectionĐơn giảnUneven distribution
Least connectionsLB chọn server có ít connection nhấtEven distributionCần LB track state
Consistent hashingHash user_id → serverReconnect lại cùng server (cache warm)Khó handle server add/remove
Least connections (recommended)

Dùng Least Connections strategy cho WebSocket LB. Tham chiếu Tuan-05-Load-Balancer. Consistent hashing (Tuan-10-Consistent-Hashing) có thể dùng nhưng complexity không đáng cho use case này — vì reconnection chỉ mất vài giây để re-subscribe.

4.4.3 Graceful Shutdown Flow

Khi cần restart WebSocket server (deploy, maintenance):

BướcActionDuration
1Mark server as “draining”LB ngừng gửi new connections đến server này
2Server gửi “reconnect” signal đến tất cả clientsClients bắt đầu reconnect đến server khác
3Đợi cho connections giảm về 0 (hoặc timeout)Max 60 giây
4Server unsubscribe tất cả Pub/Sub channelsCleanup
5Shutdown serverSafe

4.5 Scaling Redis Pub/Sub

4.5.1 Vấn đề — Single Redis Pub/Sub Bottleneck

Một Redis instance có thể handle ~500K messages/s (Pub/Sub throughput). Nhưng ta cần deliver 13.3M messages/s. Cần nhiều Redis instances.

Làm tròn lên: 30 Redis Pub/Sub instances (với buffer).

4.5.2 Sharding Strategy cho Pub/Sub Channels

StrategyMô tảƯu điểmNhược điểm
Hash-based shardingshard = hash(user_id) % NEven distribution, deterministicResharding khi add/remove nodes
Consistent hashingDùng hash ringSmooth reshardingPhức tạp hơn
Range-baseduser_id 1-1M → shard 1, 1M-2M → shard 2Đơn giảnUneven nếu user distribution không đều

Chọn: Hash-based sharding (đơn giản, đủ tốt).

Mỗi WebSocket server cần biết: channel của user X nằm trên Redis shard nào. Vì hash function là deterministic → mỗi server tính được mà không cần lookup.

flowchart LR
    subgraph "WebSocket Servers"
        WS1["WS Server 1"]
        WS2["WS Server 2"]
        WS3["WS Server 3"]
    end

    subgraph "Redis Pub/Sub Cluster"
        R1["Redis Shard 1<br/>Channels: user_1, user_31, ..."]
        R2["Redis Shard 2<br/>Channels: user_2, user_32, ..."]
        R3["Redis Shard 3<br/>Channels: user_3, user_33, ..."]
        RN["Redis Shard N<br/>..."]
    end

    WS1 -->|"PUBLISH channel:user_1"| R1
    WS1 -->|"SUBSCRIBE channel:user_2"| R2
    WS2 -->|"PUBLISH channel:user_32"| R2
    WS2 -->|"SUBSCRIBE channel:user_3"| R3
    WS3 -->|"SUBSCRIBE channel:user_1"| R1

    R1 -->|"Fan-out"| WS2 & WS3
    R2 -->|"Fan-out"| WS1 & WS3

    style R1 fill:#ef5350,color:#fff
    style R2 fill:#ef5350,color:#fff
    style R3 fill:#ef5350,color:#fff
    style RN fill:#ef5350,color:#fff

Lưu ý: Mỗi WebSocket server có thể connect đến nhiều Redis shards — vì friends của 1 user có thể nằm trên nhiều shards khác nhau. WS Server 1 serving User B phải subscribe channel:user_A trên shard 1, channel:user_C trên shard 3, v.v.

4.5.3 Redis Pub/Sub Connections Budget

Mỗi WebSocket server cần connect đến mỗi Redis shard (để subscribe/publish). Với 200 WS servers và 30 Redis shards:

Mỗi Redis instance có thể handle ~10K concurrent connections → đủ thoải mái.

4.5.4 Alternative — Dùng Message Queue thay Redis Pub/Sub?

Khía cạnhRedis Pub/SubMessage Queue (Kafka, RabbitMQ)
Delivery guaranteeAt-most-once (fire-and-forget)At-least-once (Kafka), At-most-once (RabbitMQ)
PersistenceKhông — message mất nếu subscriber offlineCó — Kafka lưu messages trên disk
LatencyCực thấp (~1ms)Cao hơn (~5-50ms tùy cấu hình)
Fan-outNative — 1 publish, N subscribers nhậnCần consumer groups hoặc topic-per-user
MemoryChỉ lưu trong memoryLưu trên disk (Kafka)
OrderingGuaranteed per channelGuaranteed per partition (Kafka)

Tại sao chọn Redis Pub/Sub?

  1. At-most-once là đủ: Mất 1 location update không sao — 30 giây sau có update mới. Không cần durability.
  2. Latency cực thấp: Redis Pub/Sub ~1ms, Kafka ~5-50ms. Cho real-time feature, 1ms matters.
  3. Không cần persistence: Ta không care location 5 phút trước. Chỉ cần latest.
  4. Đơn giản: Redis Pub/Sub là built-in, không cần deploy/manage thêm Kafka cluster.

Trade-off: Nếu cần guaranteed delivery (ví dụ: notification system), dùng Kafka. Cho Nearby Friends, Redis Pub/Sub là perfect fit vì ta ưu tiên low latencysimplicity hơn durability.

4.6 Nearby Friend Calculation — Tính khoảng cách

4.6.1 Server-side vs Client-side Calculation

ApproachMô tảƯu điểmNhược điểm
Server-side (chosen)WS server tính khoảng cách trước khi pushGiảm bandwidth — chỉ push friends trong radiusServer cần biết vị trí của subscriber
Client-sideServer push tất cả friends’ locations, client tự filterServer đơn giản hơnLãng phí bandwidth — push cả friends ở xa

Chọn server-side calculation vì:

  • Với 40 online friends, server chỉ push 5-10 friends trong radius thay vì 40 → tiết kiệm 75-87% bandwidth
  • Client (mobile) không phải xử lý nhiều → tiết kiệm battery
  • Server đã có vị trí của cả hai (sender và receiver) trong Redis cache

4.6.2 Haversine Formula

Khoảng cách giữa 2 điểm trên bề mặt Trái Đất:

Trong đó (bán kính Trái Đất), là latitude, là longitude.

Performance: Haversine là O(1) — chỉ là vài phép tính lượng giác. Tính 40 khoảng cách mất < 0.01ms. Không phải bottleneck.

4.6.3 Flow chi tiết khi nhận friend update

Khi WS Server (serving User B) nhận update từ channel:user_A:

BướcActionChi tiết
1Nhận message từ Pub/Sub{user_id: A, lat: 10.77, lng: 106.70, ts: ...}
2Lấy vị trí hiện tại của User BTừ local memory (cached khi B gửi location update)
3Tính Haversine distanced = haversine(B.lat, B.lng, A.lat, A.lng)
4So sánh với radius của Bif d <= B.radius
5aNếu trong radius → Push đến Bfriend_location {friend_id: A, distance: d}
5bNếu ngoài radius → bỏ quaKhông push, tiết kiệm bandwidth

Optimization: WS Server serving User B lưu vị trí của B trong local memory (không phải query Redis mỗi lần). Vị trí được cập nhật mỗi 30 giây khi B gửi location update. Đây là “cache tại chỗ” — zero latency.

4.7 Adding/Removing Friends — Dynamic Subscription

Khi friend relationship thay đổi (thêm bạn mới, hủy kết bạn), subscriptions phải cập nhật:

4.7.1 Thêm bạn mới

BướcActionTrigger
1User A và User B trở thành bạnFriend request accepted
2Notification gửi đến WS Server của A và BQua internal message queue
3WS Server của A subscribe channel:user_BNếu B đang online và opt-in
4WS Server của B subscribe channel:user_ANếu A đang online và opt-in
5Tính khoảng cách và push nếu cầnCả A và B nhận vị trí của nhau

4.7.2 Hủy kết bạn

BướcActionTrigger
1User A unfriend User BUI action
2Notification gửi đến WS Server của A và BQua internal message queue
3WS Server của A unsubscribe channel:user_B
4WS Server của B unsubscribe channel:user_A
5Push friend_offline đến cả A và BXóa khỏi nearby list

Lưu ý: Subscribe/unsubscribe trên Redis Pub/Sub là O(1) — cực nhanh. Không ảnh hưởng performance.

4.8 Handling Inactive Users — TTL và Cleanup

4.8.1 Vấn đề

User có thể inactive vì nhiều lý do:

Tình huốngHệ thống nhìn thấyCần xử lý
User tắt appWebSocket disconnect eventCleanup ngay
App bị kill bởi OSWebSocket disconnect event (có thể trễ)Cleanup ngay
User đi vào vùng không có mạngKhông nhận location update, heartbeat missDetect và cleanup
User để điện thoại yên 1 chỗVẫn nhận location update (vị trí không đổi)Không cần xử lý — vẫn active
User tắt GPSApp không có GPS data → ngừng gửi locationDetect và thông báo user

4.8.2 TTL Strategy

DataTTLLý do
Location cache (Redis)120 giây (2x update interval)Nếu không nhận update trong 2 chu kỳ → user đã offline
WebSocket heartbeat60 giâyServer gửi ping, client trả lời pong. Miss 2 ping liên tiếp → disconnect
Pub/Sub subscriptionKhông có TTL — cleanup khi disconnectSubscribe/unsubscribe là explicit

Flow khi user inactive:

sequenceDiagram
    participant A as User A (Client)
    participant WS as WebSocket Server
    participant Cache as Redis Cache
    participant PubSub as Redis Pub/Sub
    participant Friends as Friends' WS Servers

    Note over A,WS: User A mất mạng / tắt app

    WS->>WS: Heartbeat timeout (60s, 2 missed pings)
    WS->>WS: Close WebSocket connection

    par Cleanup
        WS->>Cache: DEL user:A:location
    and
        WS->>PubSub: UNSUBSCRIBE all friend channels
    and
        WS->>PubSub: PUBLISH channel:user_A {status: "offline"}
    end

    PubSub->>Friends: User A offline notification
    Friends->>Friends: Remove A from nearby list,<br/>push friend_offline to clients

4.8.3 Redis TTL as Safety Net

Ngay cả khi server không kịp cleanup (ví dụ server crash), Redis TTL trên location key sẽ tự động xóa data:

Friends sẽ nhận ra user “đã cập nhật lần cuối 2 phút trước” → client có thể dim hoặc ẩn user này khỏi danh sách.

4.9 Geohash Optimization — Giảm Computation

4.9.1 Vấn đề

Với mỗi location update từ friend, WS server phải tính Haversine distance. Nếu user có 40 online friends và mỗi friend update mỗi 30s, mỗi user nhận:

40 calculations mỗi 30 giây là ít — chưa phải bottleneck. Nhưng với 50K users trên 1 WS server:

Haversine là nhẹ (~0.01ms), nên 67K calculations/s chỉ mất ~0.67ms CPU time. Không phải bottleneck.

Kết luận: Cho quy mô 10M concurrent users, geohash optimization KHÔNG cần thiết. Brute-force Haversine calculation đủ nhanh. Tuy nhiên, nếu quy mô tăng lên 100M+ concurrent users, hoặc friends count tăng lên 5000+, thì geohash optimization có thể cần.

4.9.2 Geohash Optimization (nếu cần)

Nếu cần optimize (quy mô cực lớn):

Kỹ thuậtMô tảTiết kiệm
Pre-filter by geohashMỗi user có geohash (tính từ lat/lng). Chỉ tính Haversine cho friends có geohash gầnLoại 80-90% friends ở xa
Lazy calculationChỉ tính khi friend’s geohash thay đổi (không tính lại nếu vẫn ở cùng cell)Giảm 90%+ calculations cho friends ngồi yên
Batch calculationGom nhiều updates và tính 1 lần mỗi 5-10 giây thay vì mỗi updateGiảm CPU spikes

4.10 Multi-Region Architecture

4.10.1 Tại sao cần Multi-Region?

Lý doChi tiết
LatencyUser ở Việt Nam connect đến server ở US → ~200ms latency. WebSocket updates sẽ chậm
Availability1 region down → toàn bộ hệ thống down. Multi-region → failover
Data sovereigntyGDPR yêu cầu data EU users lưu ở EU
User distribution100M DAU phân bổ toàn cầu — không thể serve từ 1 region

4.10.2 Regional Architecture

flowchart TB
    subgraph "Region: US-East"
        US_LB["Load Balancer"]
        US_WS["WebSocket Servers<br/>60 servers"]
        US_Redis["Redis Cluster<br/>(Cache + Pub/Sub)"]
    end

    subgraph "Region: EU-West"
        EU_LB["Load Balancer"]
        EU_WS["WebSocket Servers<br/>50 servers"]
        EU_Redis["Redis Cluster<br/>(Cache + Pub/Sub)"]
    end

    subgraph "Region: AP-Southeast"
        AP_LB["Load Balancer"]
        AP_WS["WebSocket Servers<br/>90 servers"]
        AP_Redis["Redis Cluster<br/>(Cache + Pub/Sub)"]
    end

    subgraph "Cross-Region"
        Bridge["Cross-Region<br/>Message Bridge<br/>(for cross-region friend pairs)"]
    end

    US_Redis <-->|"Cross-region<br/>friend updates"| Bridge
    EU_Redis <-->|"Cross-region<br/>friend updates"| Bridge
    AP_Redis <-->|"Cross-region<br/>friend updates"| Bridge

    style Bridge fill:#ff9800,color:#000
    style US_Redis fill:#ef5350,color:#fff
    style EU_Redis fill:#ef5350,color:#fff
    style AP_Redis fill:#ef5350,color:#fff

4.10.3 Cross-Region Friend Pairs

Vấn đề: User A ở Việt Nam (AP-Southeast), User B ở Mỹ (US-East). Cả hai là bạn bè và bật Nearby Friends. Làm sao A nhận location của B?

ApproachMô tảƯu điểmNhược điểm
Cross-region Pub/Sub bridge (chosen)Khi A update location, publish ở AP region. Message bridge forward đến US region cho B’s subscribersTách biệt regions, bridge chỉ cho cross-region pairsThêm latency (~100-200ms cross-region)
Global Pub/Sub1 Pub/Sub cluster serve toàn cầuĐơn giảnSingle point of failure, high latency cho remote users
Ignore cross-regionChỉ hiển thị friends cùng regionĐơn giản nhấtBad UX — không thấy bạn bè ở nước khác

Trade-off: Cross-region friends sẽ nhận location update trễ hơn ~100-200ms (vì phải đi qua internet giữa regions). Nhưng vì nearby friends thường ở cùng thành phố (cùng region), đa số updates là intra-region và cực nhanh.

Thực tế: Hầu hết friend pairs cùng bật Nearby Friends sẽ ở cùng khu vực (ai bật Nearby Friends để xem bạn ở cách 10,000 km?). Cross-region pairs là edge case — có thể chấp nhận latency cao hơn.

4.10.4 Region Assignment

User được assign vào region gần nhất dựa trên IP hoặc GPS location:

User locationRegionGhi chú
Việt Nam, Thái Lan, IndonesiaAP-Southeast (Singapore)RTT ~10-30ms
Mỹ, CanadaUS-East hoặc US-WestRTT ~10-50ms
Châu ÂuEU-West (Ireland/Frankfurt)RTT ~10-30ms
Nhật Bản, Hàn QuốcAP-Northeast (Tokyo)RTT ~10-20ms

DNS-based routing (ví dụ: AWS Route 53 latency-based routing) tự động route user đến region có latency thấp nhất.


5. Estimation — Deep Dive Numbers

5.1 WebSocket Server Capacity Planning

Thêm 20% buffer cho failures và maintenance:

Memory per server:

Thêm OS, application, Redis connections: ~4 GB total. Server 8 GB RAM là đủ.

5.2 Redis Cluster Sizing

Location Cache (separate cluster):

1 Redis instance (với replication) là đủ.

Pub/Sub Cluster:

5.3 Network Bandwidth

Inbound (client → server):

Per server: — không đáng kể.

Outbound (server → client):

Per server: — chấp nhận được (server thường có 1-10 Gbps NIC).

Internal (server ↔ Redis):

Cộng với cache reads/writes:

5.4 Tóm tắt Capacity

ResourceQuantitySpec
WebSocket servers2408 GB RAM, 4 vCPU
Redis (Location Cache)3 (1 primary + 2 replicas)4 GB RAM
Redis (Pub/Sub shards)302 GB RAM each
Total Redis memory~67 GBCache (1 GB) + Pub/Sub (60 GB)
Network bandwidth (internal)~1.4 GB/s10 Gbps network
Database (User/Friends)3 (1 primary + 2 replicas)Standard PostgreSQL

6. Security — Bảo vệ Location Privacy

6.1 Opt-in / Opt-out — Nguyên tắc số 1

Nguyên tắcImplementationChi tiết
Default OFFNearby Friends tắt mặc địnhUser phải chủ động bật. Không bao giờ tự động bật
Granular controlCho phép chọn chia sẻ với ai”Chia sẻ với tất cả bạn bè” vs “Chỉ chia sẻ với Close Friends list”
Easy OFF1 tap để tắtKhông phải vào Settings → Privacy → Location → Nearby Friends → Off
Auto OFFTự động tắt sau thời gianOption: tắt sau 1h, 4h, 8h. Giống WhatsApp Live Location
Visual indicatorHiển thị rõ ràng khi đang chia sẻIcon trên status bar, periodic reminder “Bạn đang chia sẻ vị trí”

6.2 Location Precision Control — Fuzzing

LevelPrecisionUse caseImplementation
Exact~10m (GPS accuracy)Close friends, gia đìnhGửi raw GPS coordinates
Approximate~500mBạn bè bình thườngRound lat/lng đến 3 decimal places
City-level~10kmAcquaintancesChỉ gửi city name, không gửi coordinates
HiddenN/AKhông muốn chia sẻKhông gửi location, unsubscribe

Implementation: Server-side fuzzing. Client gửi raw GPS, server áp dụng precision level trước khi publish lên Pub/Sub. Như vậy client không cần biết logic — và không thể bypass.

6.3 Stalking Prevention

Mối đe dọaGiải phápChi tiết
Theo dõi liên tụcRate limit visibilityKhông cho phép user X xem vị trí của Y nhiều hơn 1 lần/phút (client-side throttle)
Location history inferenceKhông lưu historyServer chỉ lưu latest location. Không có API để lấy history. Client chỉ hiển thị real-time
Fake accountsVerificationYêu cầu phone verification để bật Nearby Friends
HarassmentBlock + ReportUser block → tự động unsubscribe cả 2 chiều. Report → review bởi trust & safety team
Ghost ModeẨn vị trí nhưng vẫn thấy bạn bèUser A bật Ghost Mode → A vẫn subscribe friends’ channels (thấy bạn bè). Nhưng A không publish location → bạn bè không thấy A
Invisible to specific peoplePer-friend setting”Ẩn vị trí với Tuấn” → unsubscribe Tuấn khỏi channel của mình

6.4 Rate Limiting Location Updates

TierLimitLý do
Per user1 location update / 10 giây (min)Ngăn client gửi quá nhiều (battery drain, bandwidth)
Per userMax 1 update / 30 giây (expected)Normal operation
Per server100K updates/sBảo vệ Redis khỏi overload
Burst3 updates trong 5 giây (khi mới bật)Cho phép initial burst để lấy vị trí nhanh

Tham chiếu Tuan-09-Rate-Limiter cho Token Bucket implementation.

6.5 Data Retention — Không lưu Location History

Dữ liệuRetentionLý do
Current location (Redis)TTL 120 giâyChỉ cần latest, tự động xóa
Location updates (logs)Không lưuPrivacy — không có lý do lưu
Aggregated analytics90 ngày”Bao nhiêu users bật Nearby Friends?” — không chứa location
Pub/Sub messages0 — fire and forgetRedis Pub/Sub không persist

GDPR Article 5(1)(e): Dữ liệu cá nhân chỉ được lưu trong thời gian cần thiết cho mục đích xử lý. Location data cho Nearby Friends chỉ cần hiện tại — không cần lưu trữ.

6.6 GDPR và Data Protection Compliance

Yêu cầuImplementation
Lawful basisConsent (opt-in). Không phải legitimate interest — location là “special category”
Right to be forgottenUser yêu cầu xóa → xóa location cache, unsubscribe tất cả channels, xóa account data
Data portabilityExport: danh sách friends đã chia sẻ location (không có location data vì không lưu)
Purpose limitationLocation chỉ dùng cho Nearby Friends feature. Không dùng cho advertising, analytics, hoặc share với third party
Data minimizationChỉ thu thập lat, lng, timestamp. Không thu thập altitude, speed, heading
Data Protection Impact Assessment (DPIA)Bắt buộc vì xử lý location data ở quy mô lớn
Data Processing AgreementVới cloud provider (AWS/GCP)

7. DevOps & Monitoring

7.1 Key Metrics — WebSocket Health

MetricAlert ThresholdDashboardÝ nghĩa
ws_connection_count per server> 55K (110% capacity)Gauge per serverServer gần đầy, cần scale out
ws_connection_count total< 8M (80% expected)Single numberCó thể có vấn đề — users không connect được
ws_connection_churn_rate> 5K/min per serverTime seriesNhiều reconnections → network issue hoặc server issue
ws_handshake_latency_p99> 500msHistogramConnection chậm → check LB, check TLS
ws_message_send_errors> 0.1%PercentageMessages không gửi được đến client

7.2 Key Metrics — Redis Pub/Sub

MetricAlert ThresholdDashboardÝ nghĩa
pubsub_channels_count> 12M (120% expected)GaugeNhiều channels → nhiều users online (tốt) hoặc leak (xấu)
pubsub_messages_per_second> 15M/s (>113% expected)Time seriesGần capacity → cần add shards
pubsub_subscribers_per_channel max> 1000HistogramPopular user → có thể cần rate limit
redis_memory_used per shard> 80% max memoryGaugeCần increase memory hoặc add shards
redis_connected_clients per shard> 8KGaugeNhiều connections từ WS servers

7.3 Key Metrics — Location Propagation

MetricAlert ThresholdDashboardÝ nghĩa
location_propagation_latency_p50> 50msHistogramTrung bình — phải < 50ms
location_propagation_latency_p99> 500msHistogramWorst case — phải < 500ms
location_propagation_latency_p999> 2sHistogramExtreme — investigate nếu > 2s
location_update_rate< 200K/s (< 60% expected)Time seriesUsers không gửi updates → client bug?
nearby_friends_count avg per userN/A (informational)HistogramTrung bình mỗi user thấy bao nhiêu friends nearby

7.4 Key Metrics — Business Health

MetricAlert ThresholdDashboardÝ nghĩa
feature_opt_in_rateN/A (informational)PercentageBao nhiêu % DAU bật Nearby Friends
avg_session_durationN/A (informational)Time seriesUsers dùng feature bao lâu
error_rate_by_type> 1% cho bất kỳ error type nàoStacked barChi tiết errors: auth, timeout, redis, etc.

7.5 Alerting Rules

AlertConditionSeverityAction
WebSocket server overloadedconnections > 55K for 5 minP2Auto-scale, add servers
Redis Pub/Sub throughput high> 90% capacity for 10 minP1Add shards, page on-call
Location propagation slowp99 > 1s for 5 minP1Check Redis, check network
Connection churn spikechurn > 10K/min for 3 minP2Check network, check DNS, check LB
Redis shard downshard unreachable for 1 minP1Auto-failover, page on-call
Cross-region bridge lag> 5s for 3 minP2Check inter-region network

Tham chiếu Tuan-13-Monitoring-Observability cho alerting best practices và runbook structure.

7.6 Deployment Strategy

ComponentStrategyLý do
WebSocket serversRolling update với graceful drainStateful connections — cần drain trước khi shutdown
Redis Pub/SubAdd shards without downtime (resharding)Không được restart — mất subscriptions
Redis CacheBlue-green với replicationFailover tới replica nếu primary down
Configuration changes (radius, TTL)Feature flags (LaunchDarkly/Unleash)Thay đổi không cần deploy
New featuresCanary 5% → 25% → 100%Phát hiện vấn đề sớm

7.7 Load Testing Considerations

ScenarioTest methodTarget
50K WebSocket connections per serverLocust/k6 WebSocket load testVerify server handles 50K connections
333K location updates/sDistributed load generatorsVerify Redis Pub/Sub throughput
Fan-out explosion (user với 5000 friends)Synthetic test với high-fan-out usersVerify no cascading failures
Server crash during peakKill 1 WS server, observe reconnectionVerify reconnection < 30s
Redis shard failureKill 1 Redis shard, observe failoverVerify auto-failover < 10s

8. Diagrams

8.1 Complete Location Update Flow

flowchart TB
    subgraph "1. Client sends location"
        Client_A["User A Mobile App"]
        GPS["GPS Module<br/>lat: 10.7769<br/>lng: 106.7009"]
        GPS --> Client_A
    end

    subgraph "2. WebSocket Server receives"
        WS_A["WS Server 1<br/>(serving User A)"]
    end

    subgraph "3. Parallel writes"
        Redis_Cache["Redis Cache<br/>SET user:A:location<br/>{lat, lng, ts}<br/>TTL=120s"]
        Redis_PubSub["Redis Pub/Sub<br/>PUBLISH channel:user_A<br/>{lat, lng, ts}"]
    end

    subgraph "4. Fan-out to subscribers"
        WS_B["WS Server 2<br/>(serving User B)<br/>subscribed to channel:user_A"]
        WS_C["WS Server 3<br/>(serving User C)<br/>subscribed to channel:user_A"]
        WS_D["WS Server 1<br/>(serving User D)<br/>subscribed to channel:user_A"]
    end

    subgraph "5. Distance check + push"
        Check_B["B at 10.78, 106.71<br/>d = 1.2km < 5mi ✓"]
        Check_C["C at 10.90, 106.85<br/>d = 20km > 5mi ✗"]
        Check_D["D at 10.77, 106.70<br/>d = 0.1km < 5mi ✓"]
    end

    subgraph "6. Client receives"
        Client_B["User B sees:<br/>A is 1.2km away"]
        Client_D["User D sees:<br/>A is 0.1km away"]
    end

    Client_A -->|"WebSocket"| WS_A
    WS_A --> Redis_Cache
    WS_A --> Redis_PubSub

    Redis_PubSub --> WS_B
    Redis_PubSub --> WS_C
    Redis_PubSub --> WS_D

    WS_B --> Check_B
    WS_C --> Check_C
    WS_D --> Check_D

    Check_B -->|"Push"| Client_B
    Check_C -->|"Skip — out of radius"| X["(no push)"]
    Check_D -->|"Push"| Client_D

    style Redis_Cache fill:#ef5350,color:#fff
    style Redis_PubSub fill:#ff7043,color:#fff
    style Check_C fill:#e0e0e0,color:#999
    style X fill:#e0e0e0,color:#999

8.2 Redis Pub/Sub Fan-out Detail

flowchart LR
    subgraph "User A updates location"
        A_Update["User A<br/>location_update<br/>{lat, lng}"]
    end

    subgraph "Redis Pub/Sub Shard 3<br/>(channel:user_A hashed to shard 3)"
        Channel_A["channel:user_A<br/>Subscribers: B, C, D, E, F"]
    end

    subgraph "Fan-out (5 messages)"
        M1["→ WS Server 2 (for B)"]
        M2["→ WS Server 3 (for C)"]
        M3["→ WS Server 1 (for D)"]
        M4["→ WS Server 4 (for E)"]
        M5["→ WS Server 2 (for F)"]
    end

    A_Update -->|"PUBLISH"| Channel_A
    Channel_A --> M1
    Channel_A --> M2
    Channel_A --> M3
    Channel_A --> M4
    Channel_A --> M5

    style Channel_A fill:#ef5350,color:#fff
    style A_Update fill:#42a5f5,color:#fff

8.3 WebSocket Server Scaling

flowchart TB
    subgraph "Load Balancer Layer"
        LB["L7 Load Balancer<br/>Least Connections strategy<br/>WebSocket upgrade support"]
    end

    subgraph "WebSocket Server Fleet (240 servers)"
        subgraph "AZ-1 (80 servers)"
            WS1_1["WS 1<br/>48K conn"]
            WS1_2["WS 2<br/>50K conn"]
            WS1_N["WS ...<br/>49K conn"]
        end
        subgraph "AZ-2 (80 servers)"
            WS2_1["WS 81<br/>50K conn"]
            WS2_2["WS 82<br/>47K conn"]
            WS2_N["WS ...<br/>50K conn"]
        end
        subgraph "AZ-3 (80 servers)"
            WS3_1["WS 161<br/>49K conn"]
            WS3_2["WS 162<br/>50K conn"]
            WS3_N["WS ...<br/>48K conn"]
        end
    end

    subgraph "Auto Scaling"
        ASG["Auto Scaling Group<br/>Min: 200 | Max: 400<br/>Target: 80% connection capacity"]
    end

    LB --> WS1_1 & WS1_2 & WS1_N
    LB --> WS2_1 & WS2_2 & WS2_N
    LB --> WS3_1 & WS3_2 & WS3_N

    ASG -.->|"Scale out/in"| WS1_N & WS2_N & WS3_N

    style LB fill:#42a5f5,color:#fff
    style ASG fill:#ff9800,color:#000

Tại sao 3 Availability Zones? Nếu 1 AZ down, 2 AZ còn lại vẫn handle 67% traffic. Với 20% buffer (240 vs 200 needed), hệ thống survive 1 AZ failure.

8.4 Full System Architecture — Production Grade

flowchart TB
    subgraph "Client Layer"
        iOS["iOS App"]
        Android["Android App"]
    end

    subgraph "Edge Layer"
        CDN["CDN<br/>(static assets)"]
        DNS["DNS<br/>Latency-based routing"]
    end

    subgraph "Gateway Layer"
        ALB["Application Load Balancer<br/>WebSocket support<br/>TLS termination"]
        AuthZ["Auth Service<br/>JWT validation"]
    end

    subgraph "Application Layer"
        WS_Fleet["WebSocket Server Fleet<br/>240 servers across 3 AZs<br/>50K connections/server"]
    end

    subgraph "Data Layer"
        Redis_Cache2["Redis Cluster<br/>Location Cache<br/>3 nodes (1P+2R)"]
        Redis_PS["Redis Pub/Sub Cluster<br/>30 shards"]
    end

    subgraph "Supporting Services"
        User_Svc["User Service<br/>(Friends list)"]
        Notif_Svc["Notification Service<br/>(push notifications)"]
        Config_Svc["Config Service<br/>(feature flags)"]
    end

    subgraph "Storage"
        PG[("PostgreSQL<br/>Users, Friends<br/>1P + 2R")]
    end

    subgraph "Observability"
        Metrics["Prometheus + Grafana"]
        Logs["ELK Stack"]
        Traces["Jaeger/Zipkin"]
    end

    iOS & Android --> DNS
    DNS --> ALB
    ALB -->|"WS Upgrade"| AuthZ
    AuthZ -->|"Valid token"| WS_Fleet

    WS_Fleet --> Redis_Cache2
    WS_Fleet --> Redis_PS
    WS_Fleet --> User_Svc
    User_Svc --> PG

    WS_Fleet -.->|"User offline > 5min"| Notif_Svc

    WS_Fleet -.->|"Metrics"| Metrics
    WS_Fleet -.->|"Logs"| Logs
    WS_Fleet -.->|"Traces"| Traces

    Config_Svc -.->|"Feature flags"| WS_Fleet

    style ALB fill:#42a5f5,color:#fff
    style Redis_Cache2 fill:#ef5350,color:#fff
    style Redis_PS fill:#ff7043,color:#fff
    style PG fill:#66bb6a,color:#fff
    style Metrics fill:#ab47bc,color:#fff

9. Aha Moments & Pitfalls

9.1 Aha Moments — Những insight quan trọng

Aha 1: Pub/Sub đơn giản hơn Polling rất nhiều

Vấn đề: Làm sao để User B biết User A vừa update location?

Cách ngây thơ: Polling — mỗi 30 giây, server của B query Redis để lấy location của tất cả 40 online friends.

Cách Pub/Sub: B subscribe channels của friends. Khi A update, B tự động nhận.

Pub/Sub giảm Redis read load từ 13.3M xuống 333K — giảm 40x. Đây là sức mạnh của event-driven architecture.

Aha 2: WebSocket là stateful — và đó là OK

Nhiều backend devs sợ stateful services vì khó scale. Nhưng cho real-time features, stateful là bắt buộc — không có cách nào khác để maintain persistent bidirectional connection.

Bài học: Không phải mọi thứ đều phải stateless. Stateful services có chỗ riêng của chúng — key là biết cách scale chúng (connection draining, least-connections LB, graceful shutdown).

Aha 3: Redis Pub/Sub không persist — và đó là ưu điểm

Nếu miss 1 location update vì Pub/Sub là fire-and-forget, không sao — 30 giây sau có update mới. Tính chất “không persist” của Redis Pub/Sub biến từ nhược điểm thành ưu điểm: không tốn disk, không cần cleanup, không cần retention policy.

So sánh: Kafka persist mỗi message, cần manage offsets, cần disk space, cần compaction. Cho Nearby Friends, tất cả những thứ đó là overhead không cần thiết.

Aha 4: Fan-out là bottleneck, không phải latency

Mỗi location update mất ~10-15ms để propagate — cực nhanh. Bottleneck là số lượng messages cần fan-out: 13.3M/s. Đây là bài toán throughput, không phải latency.

Ý nghĩa cho interview: Khi interviewer hỏi “làm sao optimize?”, đừng trả lời “giảm latency”. Trả lời “giảm fan-out” hoặc “tăng throughput của Pub/Sub layer”.

Aha 5: Server-side filtering tiết kiệm bandwidth đáng kể

Chỉ push friends trong radius thay vì tất cả friends. Với avg 40 online friends nhưng chỉ 5-10 trong radius:

75% outbound bandwidth được tiết kiệm nhờ server-side distance calculation. Cho mobile users (4G/5G có data cap), đây là rất quan trọng.

Aha 6: Không cần Geospatial Index

Khác với Proximity Service (cần geohash/quadtree để tìm trong 100M businesses), Nearby Friends chỉ cần check khoảng cách giữa user và 40 friends. 40 Haversine calculations là trivial — không cần spatial index.

Điểm khác biệt chính: Proximity Service là “tìm người lạ trong bán kính” (hay: “search in a large dataset”). Nearby Friends là “check vị trí của người đã biết” (hay: “lookup known entities”). Search cần index. Lookup không cần.

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

Pitfall 1: Dùng HTTP Polling thay vì WebSocket

HTTP Polling cho Nearby Friends là sai lầm lớn nhất. Mỗi 30 giây, 10M clients gửi HTTP request → 333K QPS. Mỗi request có ~500 bytes HTTP headers. Và server không thể push — client phải pull.

Fix: WebSocket — persistent connection, bidirectional, minimal overhead (2-6 bytes/message).

Pitfall 2: Fan-out explosion không được xử lý

Một user có 5000 friends, 500 online → mỗi 30 giây, 500 messages fan-out. Nếu 1000 users như vậy update cùng lúc:

Có thể làm Redis Pub/Sub spike và ảnh hưởng toàn hệ thống.

Fix: Rate limit fan-out cho users có nhiều friends. Hoặc dùng batching: gom updates của popular users và publish 1 lần mỗi 5-10 giây thay vì mỗi 30 giây.

Pitfall 3: Không handle WebSocket reconnection đúng cách

Server restart, network blip, app bị background → connection mất. Nếu client không reconnect hoặc reconnect quá nhanh (thundering herd), hệ thống có thể overload.

Fix: Exponential backoff với jitter. Client retry: 1s + random(0-500ms), 2s + random, 4s + random, … Max 60s.

Pitfall 4: Quên cleanup khi user offline

User tắt app nhưng server không cleanup: location cache vẫn còn (TTL chưa hết), Pub/Sub channel vẫn active, friends vẫn thấy user “online”.

Fix: 3 lớp cleanup:

  1. WebSocket disconnect event → immediate cleanup
  2. Heartbeat miss → detect và cleanup
  3. Redis TTL → safety net, tự động xóa data cũ

Pitfall 5: Lưu location history “để sau này dùng”

“Để sau này làm feature này nọ” — và rồi GDPR audit phát hiện bạn lưu vị trí của 100M users mỗi 30 giây mà không có consent.

Fix: Không lưu những gì không cần thiết. Nearby Friends chỉ cần current location. Nếu sau này cần location history → thiết kế feature riêng với consent riêng.

Pitfall 6: Location precision vs privacy trade-off không được cân nhắc

Gửi raw GPS coordinates (10 decimal places, chính xác ~0.1mm) cho tất cả friends. Không ai cần biết bạn ở đâu chính xác đến 0.1mm.

Fix: Round coordinates đến 3-4 decimal places (~10-100m precision). Đủ chính xác để hiển thị trên bản đồ. Không đủ chính xác để xác định bạn đang ở phòng nào trong tòa nhà.

Pitfall 7: Không test fan-out ở production scale

Dev test với 100 users, mỗi user 10 friends → ok. Production 10M users, mỗi user 400 friends → Redis Pub/Sub overload.

Fix: Load test với realistic numbers trước khi launch. Simulate 10M connections, 333K updates/s, 13.3M fan-out messages/s.


10. Summary — Decision Framework

10.1 Khi nào dùng Pub/Sub pattern?

Tình huốngRecommendation
Real-time updates cho known recipientsPub/Sub — subscribers đã biết trước
Fan-out nhiều nhưng message không cần durableRedis Pub/Sub — fire-and-forget
Cần guaranteed deliveryKafka/RabbitMQ thay vì Redis Pub/Sub
1-to-1 messagingWebSocket trực tiếp, không cần Pub/Sub

10.2 Khi nào dùng WebSocket?

Tình huốngRecommendation
Server cần push data đến clientWebSocket
Chỉ client → serverHTTP là đủ
Chỉ server → clientSSE (Server-Sent Events) có thể đủ
Cả hai chiều + low latencyWebSocket — đây là Nearby Friends
Low frequency updates (mỗi vài phút)Long polling có thể đủ

10.3 Tổng kết Architecture Decisions

DecisionChosenAlternativeTại sao
Communication protocolWebSocketHTTP Polling, SSEBidirectional, low overhead
Location propagationRedis Pub/SubKafka, RabbitMQLow latency, no persistence needed
Location storageRedis (in-memory cache)DatabaseChỉ cần latest, TTL tự động xóa
Distance calculationServer-side HaversineClient-side, Geohash pre-filterGiảm bandwidth, 40 calculations là trivial
Channel design1 channel per userPer geohash, per friend-pairGranular, privacy-friendly
Scaling WebSocketLeast-connections LBConsistent hashingĐơn giản, reconnection cost thấp
Scaling Pub/SubHash-based shardingConsistent hashingĐơn giản, deterministic
Multi-regionRegional clusters + cross-region bridgeGlobal clusterLow latency cho local, bridge cho remote

TopicLinkLiên quan
Proximity Service (businesses)Case-Design-Proximity-ServiceSo sánh static vs dynamic location, geohash indexing
Chat System (WebSocket)Tuan-17-Design-Chat-SystemWebSocket management, connection lifecycle, message delivery
Consistent HashingTuan-10-Consistent-HashingShard assignment cho Redis Pub/Sub, server routing
Load BalancerTuan-05-Load-BalancerWebSocket-aware LB, least-connections strategy
Message QueueTuan-08-Message-QueuePub/Sub vs Message Queue trade-offs
Cache StrategyTuan-06-Cache-StrategyRedis caching patterns, TTL strategy
Rate LimiterTuan-09-Rate-LimiterRate limiting location updates, fan-out throttling
MonitoringTuan-13-Monitoring-ObservabilityWebSocket metrics, Pub/Sub monitoring, alerting
SecurityTuan-14-AuthN-AuthZ-SecurityJWT cho WebSocket auth, privacy controls
Database ReplicationTuan-07-Database-Sharding-ReplicationRedis replication, PostgreSQL for user data

12. Interview Tips — Gợi ý cho phỏng vấn

12.1 Cấu trúc trả lời

BướcNội dungThời gian
1. Clarify requirementsHỏi về scale, features, constraints3-5 phút
2. High-level designWebSocket + Redis Cache + Redis Pub/Sub5-7 phút
3. Deep diveChọn 2-3 topics để đi sâu (Pub/Sub fan-out, scaling WebSocket, privacy)15-20 phút
4. Wrap upTrade-offs, alternatives, monitoring3-5 phút

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

Câu hỏiHướng trả lời
”Tại sao không dùng HTTP polling?”Bandwidth waste, server cần push, bidirectional need
”Làm sao scale WebSocket?”Least-connections LB, graceful drain, auto-scaling
”Nếu Redis Pub/Sub down?”Graceful degradation — nearby list freeze, client show “last updated X min ago"
"Làm sao xử lý popular users?”Rate limit fan-out, batch updates, reduce frequency
”Privacy concerns?”Opt-in, fuzzing, no history, GDPR, Ghost Mode
”Tại sao không dùng geohash?”Chỉ 40 friends — brute force Haversine đủ nhanh, không cần spatial index
”Alternative cho Redis Pub/Sub?”Kafka (nếu cần durability), custom in-memory Pub/Sub (nếu cần ultra-low latency)

12.3 Điểm thưởng (Bonus Points)

TopicChi tiếtẤn tượng
Battery optimizationLocation updates mỗi 30s thay vì liên tục. Dùng significant location change API của iOS/AndroidShow mobile awareness
Graceful degradationRedis down → serve stale data, show “approximate”Show resilience thinking
A/B testingTest 30s vs 60s update interval, test radius defaultsShow product thinking
Cost estimation240 WS servers x 17K/monthShow business awareness

“Nearby Friends không phải là bài toán khó về algorithm — không có geohash, không có quadtree, không có Dijkstra. Nó khó ở real-time communication ở quy mô lớn: 10 triệu WebSocket connections, 333 nghìn location updates mỗi giây, 13 triệu Pub/Sub messages mỗi giây. Đây là bài toán về infrastructure và engineering trade-offs, không phải về toán học.”