Skip to content

Chat Application System Design (HLD & LLD)

  • 1-to-1 messaging
  • Group chat
  • Send/receive messages in real-time
  • Message delivery status:
    • Sent
    • Delivered
    • Read
  • Online/offline presence
  • Message history
  • Push notifications
  • Media messages
  • Message reactions
  • Typing indicators
  • Message editing/deleting
  • End-to-end encryption

  • Low latency (<100 ms)
  • Highly scalable (millions of users)
  • Highly available
  • Eventual consistency
  • Reliable message delivery
  • Horizontal scalability

flowchart TD
    C[Client Mobile/Web]
    G[API Gateway]
    CS[Chat Service]
    PS[Presence Service]
    MS[Message Service]
    NS[Notification Service]
    R[(Redis)]
    K[(Kafka)]
    U[(User DB)]
    MDB[(Message DB)]
    PUSH[Push Service]

    C --> G --> CS
    CS --> PS
    CS --> MS
    CS --> NS
    PS --> R
    MS --> K
    MS --> MDB
    NS --> PUSH
    PS --> U
  • Authentication
  • Rate limiting
  • Routing

Responsible for:

  • Sending messages
  • Receiving messages
  • Routing messages

Technologies:

  • WebSocket
  • gRPC
  • HTTP

Tracks:

  • Online/offline status
  • Last seen

Redis example:

userId -> online
userId -> last_seen

Responsible for:

  • Message persistence
  • Message retrieval
  • Delivery status

Responsible for:

  • Push notifications
  • Email/SMS alerts

Used for:

  • Decoupling services
  • Asynchronous message delivery
Producer -> Chat Service
Consumer -> Message Service
Consumer -> Notification Service

flowchart LR
    A[User A] --> W[WebSocket Gateway]
    W --> C[Chat Service]
    C --> K[(Kafka)]
    K --> M[Message Service]
    K --> N[Notification Service]
    M --> DB[(Message DB)]

user_id
name
phone
status
created_at
conversation_id
type (DIRECT/GROUP)
created_at
conversation_id
user_id
joined_at
message_id
conversation_id
sender_id
content
type (TEXT/IMAGE)
timestamp
status
message_id
user_id
status (sent/delivered/read)
timestamp
erDiagram
    USER ||--o{ CONVERSATION_MEMBER : joins
    CONVERSATION ||--o{ CONVERSATION_MEMBER : contains
    CONVERSATION ||--o{ MESSAGE : has
    MESSAGE ||--o{ MESSAGE_STATUS : tracks

sequenceDiagram
    participant A as User A
    participant WS as WebSocket Gateway
    participant CS as Chat Service
    participant K as Kafka
    participant MS as Message Service
    participant B as User B

    A->>WS: SEND_MESSAGE
    WS->>CS: Validate request
    CS->>K: Publish MESSAGE_SENT
    K->>MS: Consume event
    MS->>MS: Persist message
    CS->>B: Deliver via WebSocket
    B->>CS: Delivered / Read Ack
{
"senderId": "...",
"conversationId": "...",
"message": "Hello"
}

Steps:

  1. Client sends the message over WebSocket.
  2. Chat Service validates the user.
  3. Generates a messageId.
  4. Publishes MESSAGE_SENT to Kafka.
  5. Message Service stores the message.
  6. Chat Service delivers it over WebSocket.
  7. Delivery/read status is updated.

Maintain active sessions:

userId -> websocket session

Distributed registry:

userId -> serverId

Redis Cluster stores the mapping so any chat server can locate a user’s active connection.


flowchart TD
    LB[Load Balancer]
    LB --> S1[Chat Server 1]
    LB --> S2[Chat Server 2]

WebSockets require sticky sessions.

Possible solutions:

  • Consistent hashing
  • Redis-based connection registry

Shard messages by:

conversation_id

or

user_id

Cache:

recent messages
user presence
conversation metadata

Kafka partitions by conversation:

partition = hash(conversationId)

This preserves ordering for messages within a conversation.

  1. Persist the message.
  2. Send a push notification.
  3. Deliver when the user reconnects.
  • Unique messageId
  • Idempotent consumers

class Message {
String messageId;
String conversationId;
String senderId;
String content;
MessageType type;
long timestamp;
}
class Conversation {
String conversationId;
ConversationType type;
List<User> participants;
}
class ChatService {
MessageRepository messageRepository;
WebSocketManager socketManager;
public void sendMessage(Message msg){
messageRepository.save(msg);
socketManager.deliver(msg);
}
}
class WebSocketManager {
Map<String, Session> userSessions;
void deliver(Message msg){
Session s = userSessions.get(msg.receiverId);
s.send(msg);
}
}
classDiagram
    class Message
    class Conversation
    class ChatService
    class WebSocketManager
    ChatService --> WebSocketManager
    ChatService --> Message
    Conversation --> Message

Problem Solution
WebSocket scaling Connection registry
Database growth Sharding
Message ordering Kafka partitions
Offline users Push notifications
Hot conversations Partitioning

Terminal window
POST /messages

Request:

{
"conversationId": "...",
"message": "Hello"
}
Terminal window
GET /messages?conversationId=123&limit=50
Terminal window
POST /messages/read

  • How does WhatsApp scale to billions of users?
  • How would you implement end-to-end encryption?
  • How do you maintain message ordering across servers?
  • How do typing indicators work?
  • How do you support group chats with thousands of users?
  • How would you implement full-text message search?

Senior/staff-level interviews often explore:

  • WhatsApp-scale architecture
  • Fan-out vs. pull messaging models
  • Read receipt scaling
  • Millions of concurrent WebSocket connections
  • Multi-region/global chat architectures

  • WebSockets provide low-latency real-time messaging.
  • Kafka decouples services and preserves message ordering using conversation-based partitioning.
  • Redis enables presence tracking, connection routing, and caching.
  • Database sharding and horizontal scaling are essential for handling billions of messages.
  • Reliable delivery requires unique message IDs, persistent storage, and idempotent processing.