Back to System Design Problems
Q1Question 1 of 13

Design a Chat/Messaging System (like WhatsApp)

Key Concepts
  • WebSocket connections provide bidirectional, real-time message delivery; a presence registry maps users to their server.
  • Persist every message before attempting delivery so a server crash never loses it (persist-first rule).
  • Delivery status is a state machine: sent (persisted) -> delivered (device ack) -> read (user opens chat).
  • Offline queuing stores messages for disconnected users and flushes them in order on reconnect.
  • Group chat fans a message to all members, either by per-member inbox copies or a shared reference.
  • End-to-end encryption means the client encrypts with the recipient's public key; the server stores only ciphertext.
  • Messages carry monotonic sequence numbers per conversation for gap detection and duplicate rejection.

Definition

Keep a live WebSocket per user for instant delivery, persist every message, track sent-delivered-read ticks, and queue messages for offline users.
  • WebSocket : a persistent bidirectional connection for real-time message push without polling.
  • Presence registry : maps each connected user to the server holding their socket (usually Redis).
  • Delivery status : a state machine tracking sent, delivered, and read states via acknowledgements.
  • Offline queuing : stores messages for disconnected users and delivers them in order on reconnect.
  • End-to-end encryption (E2EE) : the client encrypts the message so only the recipient's device can decrypt it; the server sees only ciphertext.

National Bank of India adds a secure in-app chat so Rajesh can talk to his relationship manager Priya about a loan. When both are online, messages appear instantly. When Priya is away, the bank holds Rajesh's messages and delivers them when she returns. Rajesh sees ticks: sent, delivered, read. Because it is banking, only they can read the contents.

Understanding the Concepts

How does real-time delivery work?

Each connected user maintains a WebSocket to a gateway server. A presence registry in Redis maps each user to their server so the system knows where to route messages. When Rajesh sends a loan inquiry to Priya, the gateway looks up Priya's server and pushes the message instantly.

JAVA
package com.bank.chat;

public class MessageRouter {
    public void deliver(String recipientId, Message msg) {
        Session session = sessionRegistry.find(recipientId);
        if (session != null) session.push(msg);        // online: push now
        else offlineQueue.enqueue(recipientId, msg);   // offline: store
    }
}

Why persist before delivery?

Every message is written to durable storage before any delivery attempt. If the gateway server crashes mid-delivery, the message is safe in the database. This persist-first rule is the most important invariant in the chat system. At National Bank of India, losing a customer's loan discussion message is unacceptable.

JAVA
package com.bank.chat;

public class MessageService {
    public void send(Message msg) {
        messageStore.save(msg);           // persist first
        msg.markSent();                   // status: SENT
        router.deliver(msg.recipientId(), msg);
    }
}

How does delivery status work?

Status is a state machine driven by acknowledgements over the WebSocket. When the server persists the message, the status becomes SENT (single tick). When the recipient's device acknowledges receipt, it becomes DELIVERED (double tick). When the recipient opens the conversation, a read receipt marks it READ (blue ticks). Each transition is stored durably so the sender always sees accurate ticks.

JAVA
package com.bank.chat;

public class DeliveryStatusMachine {
    public void onPersisted(String msgId)   { updateStatus(msgId, Status.SENT);      }
    public void onDeviceAck(String msgId)   { updateStatus(msgId, Status.DELIVERED); }
    public void onReadReceipt(String msgId) { updateStatus(msgId, Status.READ);      }

    private void updateStatus(String msgId, Status newStatus) {
        messageStore.setStatus(msgId, newStatus);
        notifySender(msgId, newStatus);  // push tick update to sender
    }
}

How does offline queuing work?

Messages for disconnected users are stored in their offline queue, ordered by sequence number. When the user reconnects, the queue is flushed in order and then cleared. The queue must be durable so a server restart does not lose pending messages. At National Bank of India, when Priya's phone is off, Rajesh's loan queries wait safely until she comes back online.

JAVA
package com.bank.chat;

import java.util.List;

public class OfflineQueueService {
    public void enqueue(String userId, Message msg) {
        offlineStore.append(userId, msg);  // durable, ordered append
    }
    public List<Message> flush(String userId) {
        List<Message> pending = offlineStore.fetchAllOrdered(userId);
        offlineStore.clear(userId);
        return pending;  // delivered in sequence order on reconnect
    }
}

How does group chat work?

A group message is fanned out to all members. The fan-out can copy the message into each member's inbox or store it once and reference it from a shared group timeline. The inbox approach is simpler for small groups. The shared-reference approach saves storage for large groups like a bank's department-wide channel with hundreds of employees.

JAVA
package com.bank.chat;

import java.util.List;

public class GroupChatService {
    public void sendGroupMessage(String groupId, Message msg) {
        messageStore.save(msg);  // persist once
        List<String> members = groupStore.members(groupId);
        for (String memberId : members) {
            if (!memberId.equals(msg.senderId())) {
                router.deliver(memberId, msg);  // fan-out to each member
            }
        }
    }
}

How does end-to-end encryption work?

The client encrypts the message with the recipient's public key before sending. The server stores and forwards ciphertext it cannot read. For group chat, a shared symmetric group key is distributed to members and rotated when membership changes. In a banking context, this means the server never sees loan amounts or account details discussed in chat.

JAVA
package com.bank.chat;

import javax.crypto.Cipher;
import java.security.PublicKey;

public class E2EEncryptor {
    public byte[] encrypt(byte[] plaintext, PublicKey recipientKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, recipientKey);
        return cipher.doFinal(plaintext);
    }
}

How do you handle ordering and deduplication?

Messages carry a monotonic sequence number per conversation. The client detects gaps (missing sequence numbers) and requests retransmission. A stable message id ensures resends do not create duplicates. The server rejects a message if its id already exists in the conversation store.

JAVA
package com.bank.chat;

public class MessageDeduplicator {
    public boolean isDuplicate(String conversationId, String messageId) {
        return messageStore.exists(conversationId, messageId);
    }
}

How do you handle multi-device delivery?

Rajesh may be logged in on his phone and laptop simultaneously. Both devices must receive every message. The presence registry stores all active sessions per user, and the router pushes to every session. If no sessions are active, the message goes to the offline queue.

JAVA
package com.bank.chat;

import java.util.List;

public class MultiDeviceRouter {
    public void deliver(String userId, Message msg) {
        List<Session> sessions = sessionRegistry.findAll(userId);
        if (sessions.isEmpty()) {
            offlineQueue.enqueue(userId, msg);
        } else {
            for (Session s : sessions) s.push(msg);
        }
    }
}

What should a senior engineer know?

A senior engineer designs for WebSocket load balancing with sticky routing, handles multi-device delivery, and ensures the presence registry is eventually consistent with stale-entry cleanup from crashed servers. They also plan message retention policies, archive old conversations to cold storage, and shard the message database by conversation id for horizontal scale.

Visual Overview

DIAGRAM
  Chat System Architecture

  ┌──────────┐  WebSocket  ┌──────────┐  WebSocket  ┌──────────┐
  │  Rajesh  │◄───────────►│ Gateway  │◄───────────►│  Priya   │
  │  (client)│             │ Server 1 │             │  (client)│
  └──────────┘             └────┬─────┘             └──────────┘
                                │
                   ┌────────────┼────────────┐
                   │            │            │
            ┌──────▼─────┐ ┌───▼──────┐ ┌───▼──────┐
            │  Message   │ │ Presence │ │ Offline  │
            │  Store     │ │ Registry │ │ Queue    │
            │ (persist   │ │ (Redis)  │ │ (per-user│
            │  first)    │ │ user->srv│ │  inbox)  │
            └────────────┘ └──────────┘ └──────────┘

  Delivery status:
  Client ──send──► Server ──persist──► SENT (tick 1)
  Server ──push──► Recipient device ──ack──► DELIVERED (tick 2)
  Recipient opens chat ──read receipt──► READ (tick 3)

Key Takeaway

A chat system delivers messages in real time using WebSockets, persists every message before delivery, tracks delivery status through sent-delivered-read, and queues messages for offline users. The persist-first rule is non-negotiable: a crash must never lose a message.

Delivery status is a small state machine driven by acknowledgements. Offline queuing holds messages for disconnected users and flushes in order on reconnect. End-to-end encryption means the server only stores ciphertext.

At National Bank of India, this powered secure chat between customers and relationship managers, where offline queuing and delivery ticks were essential for trust.

Remember one sentence: persist every message before delivery, use WebSockets for real-time push, track sent-delivered-read status, and queue for offline users with end-to-end encryption for banking privacy.

---

Share
Use ← → arrow keys to navigate