Designing Chat & Messaging Apps

📖 Concept

Designing a chat system is one of the most common mobile system design questions. It tests your knowledge of real-time communication, message ordering, delivery guarantees, and offline handling.

Key components:

  1. Message delivery pipeline: Client → Server → Recipient (via push or WebSocket)
  2. Message states: Sent → Delivered → Read (WhatsApp-style ticks)
  3. Persistence: Local SQLite/Room for message history
  4. Real-time: WebSocket for active sessions, FCM for background delivery
  5. Media: Separate upload pipeline for images/videos

Architecture:

┌──────────────────────────────────┐
│  Chat UI (LazyColumn)           │
│     ↕                           │
│  ChatViewModel                  │
│     ↕                           │
│  MessageRepository              │
│     ↕           ↕               │
│  Room DB      ConnectionManager │
│  (history)      ↕       ↕       │
│              WebSocket   FCM    │
│              (online)  (offline)│
└──────────────────────────────────┘

Message ordering challenges:

  • Network delays can cause messages to arrive out of order
  • Solution: Server-assigned sequential IDs + client-side sorting
  • Optimistic: show sent messages immediately with local timestamp, reconcile with server order later

Delivery guarantees:

  • At-most-once: Fire and forget. Messages may be lost.
  • At-least-once: Retry until acknowledged. May deliver duplicates.
  • Exactly-once: Deduplicate using message IDs. Most complex but required for chat.

💻 Code Example

codeTap to expand ⛶
1// Chat system architecture — key components
2
3// Message entity with delivery state
4@Entity(tableName = "messages")
5data class MessageEntity(
6 @PrimaryKey val id: String = UUID.randomUUID().toString(),
7 val conversationId: String,
8 val senderId: String,
9 val content: String,
10 val type: String = "TEXT", // TEXT, IMAGE, VIDEO
11 val localTimestamp: Long = System.currentTimeMillis(),
12 val serverTimestamp: Long? = null,
13 val deliveryState: String = "SENDING", // SENDING, SENT, DELIVERED, READ, FAILED
14 val mediaUrl: String? = null,
15 val localMediaPath: String? = null
16)
17
18// WebSocket connection manager
19class ChatConnectionManager @Inject constructor(
20 private val okHttpClient: OkHttpClient,
21 private val messageDao: MessageDao
22) {
23 private var webSocket: WebSocket? = null
24 private val _incomingMessages = MutableSharedFlow<MessageEntity>()
25 val incomingMessages: SharedFlow<MessageEntity> = _incomingMessages
26
27 fun connect(authToken: String) {
28 val request = Request.Builder()
29 .url("wss://chat.example.com/ws")
30 .addHeader("Authorization", "Bearer $authToken")
31 .build()
32
33 webSocket = okHttpClient.newWebSocket(request, object : WebSocketListener() {
34 override fun onMessage(ws: WebSocket, text: String) {
35 val message = Json.decodeFromString<MessageEntity>(text)
36 CoroutineScope(Dispatchers.IO).launch {
37 messageDao.upsert(message)
38 _incomingMessages.emit(message)
39 }
40 }
41
42 override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
43 // Reconnect with exponential backoff
44 scheduleReconnect()
45 }
46 })
47 }
48
49 fun sendMessage(message: MessageEntity) {
50 val json = Json.encodeToString(message)
51 val sent = webSocket?.send(json) ?: false
52 if (!sent) {
53 // Queue for retry when connection restored
54 CoroutineScope(Dispatchers.IO).launch {
55 messageDao.upsert(message.copy(deliveryState = "FAILED"))
56 }
57 }
58 }
59}
60
61// Chat Repository — offline-first messaging
62class ChatRepository @Inject constructor(
63 private val messageDao: MessageDao,
64 private val connectionManager: ChatConnectionManager,
65 private val mediaUploader: MediaUploader
66) {
67 fun getMessages(conversationId: String): Flow<List<MessageEntity>> =
68 messageDao.getByConversation(conversationId)
69
70 suspend fun sendTextMessage(conversationId: String, content: String) {
71 val message = MessageEntity(
72 conversationId = conversationId,
73 senderId = currentUserId,
74 content = content,
75 deliveryState = "SENDING"
76 )
77 // Optimistic: save locally first (appears in chat immediately)
78 messageDao.upsert(message)
79 // Then send via WebSocket
80 connectionManager.sendMessage(message)
81 }
82}

🏋️ Practice Exercise

Practice:

  1. Design the message delivery pipeline with SENT/DELIVERED/READ states
  2. Implement WebSocket reconnection with exponential backoff
  3. Handle message ordering when messages arrive out of order
  4. Design the media upload pipeline for images in chat
  5. How would you implement end-to-end encryption for messages?

⚠️ Common Mistakes

  • Not handling WebSocket disconnection — mobile connections are unreliable, always implement reconnection

  • Using only WebSocket without FCM fallback — background apps can't maintain WebSocket connections

  • Not deduplicating messages — at-least-once delivery requires idempotent message handling

  • Displaying messages by local timestamp instead of server timestamp — causes ordering issues

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Designing Chat & Messaging Apps. Login to unlock this feature.