Whatsapp Chat Messaging
Requirements
Functional
1. Two Users can send communicate using chat messages.
2. Group chats supported.
3. Users can see chat history
Non Functional
1. Minimum Latency, should be near real time messaging
2. Reliable. Messages should not be lost
BOE
DAU:
- World population 7 billion
- 20-30% actively use 210 Million
Incoming bandwidth
- User sends(chat,image,video) approx(100kb/day)
- 210Mx100K = 210,000 GB/day = 210TB
- 5 years = 210TB x 30 x 12 x 5 = 378 PB
- Per second = 2.4GB/sec
Storage estimates:
- horizontally scalable nosql. recycled data
HLD
Simple 2 Users Design
User Authentication
User installs a WhatsApp messaging app on his phone, the app comes with
a pre-configured host name.
When user enters his username and password and clicks on login, app gets
the IP address from the DNS, IP resolves to the AWS API Gateway or Nginx
LB.
Password Hash travels on wire once the user is authenticated, a JWT
token is provided to the app, which is stored in the secure store.
User-A sending "Hi" to User-B
User-A finds User-B from phone contacts stored, clicks chat on the
WhatsApp and sends "Hi"
{src=User-A, dst=User-B, Msg=Hi, timestamp=..}
HTTPS message will reach API gateway, ingestion will receive HTTP
message.
Ingestion will store message on the DB.
{src=User-A, dst=User-B, Msg=Hi, timestamp=.., delivered=false}
Fan-out service checks the DB every 2 seconds Once it finds
delivered=false it will get ExpoPush Token from ExpoTokens
table and send notification(via ExpoNotification Service) to User-B
sequenceDiagram
autonumber
actor ClientA as WhatsApp Client A
box AWS
participant Gateway as API Gateway
participant Ingestion@{"type":"control"} as Ingestion Service
participant DB@{"type":"database"} as DB
participant FanOut@{"type":"control"} as Fan-out Service
end
participant ExpoServer as Expo Push Server
actor ClientB as WhatsApp Client B
ClientB->>ExpoServer: Register for push notifications
ExpoServer-->>ClientB: Return Expo Push Token
ClientB->>Ingestion: POST /api/v1/expo-token (Token, User-B ID)
Note over DB: ExpoTokens Table
Ingestion->>DB: Store token
Note over ClientA: Finds User-B from contacts
types "Hi" & sends
ClientA->>Gateway: HTTPS POST Message payload
{src: User-A, dst: User-B, Msg: "Hi", timestamp}
Gateway->>Ingestion: HTTP Payload
Note over DB: Message table
Ingestion->>DB: Insert record
{src: User-A, dst: User-B, Msg: "Hi", timestamp, delivered: false}
Ingestion-->>Gateway: 200 OK (Message Received)
Gateway-->>ClientA: 200 OK (Sent)
Note over FanOut: Check DB every 2 sec
Note over DB: Message Table
FanOut->>DB: Select * from MessageTable where delivered = false
DB-->>FanOut: {src: User-A, dst: User-B, Msg: "Hi", ...}
Note over DB: Expo Token Table
FanOut->>DB: select from ExpoToken where dst=userB
DB-->>FanOut: ExpoPush Token
FanOut->>ExpoServer: HTTP POST expoURL {Token, URL}
ExpoServer-->>ClientB: Message
Scaling to 100 M Users
flowchart LR
A["User A Phone"]
B["User B Phone"]
GLB["Global / Regional
Load Balancer"]
API["API Gateway
Nginx"]
subgraph K8S["Kubernetes Cluster"]
ING["Ingress
Controller"]
subgraph INGEST["Message Ingestion Service"]
direction TB
I1["Pod 1"]
IN["Pod N"]
I1 ~~~ IN
end
RP[["Kafka / Redpanda"]]
subgraph HISTORY["Messge Storage Service"]
direction TB
H1["Pod 1"]
HN["Pod N"]
H1 ~~~ HN
end
subgraph FANOUT["Fanout Service"]
direction TB
F1["Pod 1"]
FN["Pod N"]
F1 ~~~ FN
end
MSGDB[("Message History DB
ScyllaDB / Cassandra")]
REDIS[("Redis
Connection Registry")]
subgraph WS["WebSocket Service"]
direction TB
W1["Pod 1"]
WN["Pod N"]
W1 ~~~ WN
end
subgraph NOTIFY["Notification Service"]
direction TB
N1["Pod 1"]
N2["Pod N"]
N1 ~~~ N2
end
DEVICE[("Device / Push Token DB")]
end
EXPO["Expo / FCM / APNs"]
A -->|"HTTPS POST /message"| GLB
GLB --> ING
ING --> INGEST
INGEST -->|"Topic:
MessageCreated"| RP
RP -->|"Topic:
MessageCreated"| HISTORY
HISTORY -->|"Store Message"| MSGDB
RP -->|"Topic:
MessageCreated"| FANOUT
FANOUT --> REDIS
REDIS -->|"B connection info"| FANOUT
FANOUT -->|"Online delivery"| WS
WS -->|"WebSocket frame"| B
FANOUT -->|"B offline"| NOTIFY
NOTIFY --> DEVICE
NOTIFY --> EXPO
EXPO -->|"Push notification"| B
Sequence Diagram
Topics, Partitions are created at Kafka boot
deploy.sh
#!/usr/bin/env bash
BROKER="${KAFKA_BROKER:-kafka.ib.svc.cluster.local:9092}"
create() {
local topic=$1 parts=$2
echo "Creating topic: $topic ($parts partitions)"
kubectl exec -n ib deploy/kafka -- \
rpk topic create "$topic" -p "$parts" --brokers "$BROKER" 2>/dev/null || \
}
create identity-events 24
$ ./deploy/local/create-topics-local.sh
Redpanda
Redpanda over kafka: Due to predictable latencies
ScyllaDB(NoSQL DB)
History messages would be huge, SQL cannot scale, even messages are immutable
Over SQL: Because noSQL(implemented using
LSM Tree) can
scale horizontally while SQL(implemented using
BTree)
cannot
Over Cassandra:
Seastar Framework is fast wrt Java
JVM
Redis
For storage of User to connection-id mapping
User can connect to any pod(out of 20), fanout service should know the pod to which message to be sent
Websocket vs Expo
Expo / FCM(Firebase Cloud Messaging) is used for sending notifications to device which is offline.
it holds the notification for 30 days
When device is connected, Web socket is used to send message to the mobile device.
@startuml
autonumber
actor "User A Phone" as A
participant "Global / Regional LB" as LB
participant "API Gateway\nIngress" as API
box "Kubernetes Cluster"
participant "Ingestion Svc\nMulti-Pod" as ING
participant "Storage Svc\nMulti-Pod" as HIST
participant "Fanout Svc\nMulti-Pod" as FAN
participant "Kafka / Redpanda" as RP
database "Message History DB\nScyllaDB" as DB
database "Redis\nConnection Registry" as REDIS
database "Expo Token DB" as DEVICE
participant "WebSocket Svc\nMulti-Pod" as WS
participant "Notification Svc\nMulti-Pod" as NOTIFY
end box
actor "User B Phone" as B
participant "Expo Svc\nFCM" as PUSH
B -> WS: Authenticate (user,passwdhash)
WS --> B: JWT + Send ExpoToken\nWebsocket Estb
B -> PUSH: Give ExpoToken
PUSH --> B: ExpoToken
B -> WS: ExpoToken
WS -> DEVICE: ExpoToken for B
WS -> REDIS: SET\nKey:(User=B)\nvalue:(pod=WS-17, conn=conn-b)
note over A: A wants to send "Hi Bob"\nA and B are friends
A -> LB: POST /v1/messages\nAuthorization: Bearer token\nuser=B\nmessage="Hi Bob"
LB -> API: HTTPS POST /v1/messages
API -> ING: HTTP Req
note over ING: Validate JWT token\nDerive sender=A\nValidate payload
ING -> ING: Create message_id=msg-789
note over RP: Topic: message-created\nPartition selected using conversation_id
ING -> RP: Topic:Msg-Created, {Payload=..}
RP --> ING: ACK
ING --> API: 202 Accepted
API --> A: Message accepted
RP -> HIST: Topic=MessageCreated
note over HIST: Deserialize event\nValidate / deduplicate
HIST -> DB: INSERT message
DB --> HIST: Stored
RP -> FAN: MessageCreated event
note over FAN: Topic=MessageCreated\nUser=B
FAN -> REDIS: GET connection:B
REDIS --> FAN: pod=WS-17\nconn=conn-b
FAN -> WS: DeliverMessage\nconn=conn-b\nmessage_id=msg\nsender=A123\n"Hi Bob"
WS -> B: Message=Hi\nsrc=A
B --> WS: ACK
@enduml