Study interactive :: Progress tools open in the Study Hub reader.

24. Design a Group Chat System

Slack, Discord, WhatsApp, Microsoft Teams. Users join rooms, send messages, see typing indicators and read receipts. The expectation: messages arrive within a second or two, anywhere in the world.

The challenge isn't the algorithm. It's that millions of long-lived connections must each receive only the messages they care about.

Clarify

Question Example answer
1:1 DMs or rooms or both? Both
Max room size? 100K members (large communities)
History? Yes, infinite scroll back
Media? Images, files, short video
Reactions / replies? Yes
Typing indicators / presence? Yes
End-to-end encryption? Not for v1

Estimate

Messages add up fast. Plan for cold storage and per-room sharding.

High-level design

WebSocketClientGatewayChat APIKafka messagesPostgres or CassandraRedis presencePub/sub to gatewaysOther clients

WebSockets (Chapter 07) for the live connection. Kafka for the durable backbone. Cassandra-style storage for messages (Chapter 15) because the access pattern is "give me last N messages of room X."

Deep dive 1: Connection layer

Each online user holds a WebSocket to a gateway node. Gateways are stateless w.r.t. the user (any gateway can hold any user) but the routing table (which user is on which gateway) must be globally known.

GatewayPub/sub backboneGateway

When a message arrives:

  1. API persists it.
  2. API publishes room:42 to the pub/sub backbone.
  3. Every gateway subscribed to room:42 pushes to its connected users in that room.

Gateways subscribe only to rooms with at least one online member they serve.

Deep dive 2: Message storage

Messages have a simple access pattern: "last 50 messages in room X, before timestamp T." Wide-column databases (Cassandra, ScyllaDB) shine here.

Partition key:  room_id
Clustering key: created_at DESC
Columns:        message_id, author_id, body, attachments

For Postgres-style, shard by room_id (Chapter 16). For very large rooms, sub-shard by time bucket.

CREATE TABLE messages (
    room_id     BIGINT,
    created_at  TIMESTAMPTZ,
    message_id  BIGINT,
    author_id   BIGINT,
    body        TEXT,
    PRIMARY KEY (room_id, created_at, message_id)
);

Reads pull the latest page. Older history can move to cheaper storage after 30 days.

Deep dive 3: Presence and typing

Presence is "who is online". Not durable. Living in memory is fine.

presence:user:42  ->  "online", TTL 30s

Clients heartbeat every 15 seconds. Miss two beats and you're "away."

Typing indicators are noisier. Don't store them. Publish user 42 typing in room 7 to the pub/sub backbone, gateways forward, clients show the dot. Drop on the floor if the connection blinks.

Deep dive 4: Read receipts and unread counts

For each (user, room) pair, store the last read message ID.

HSET reads:user:42  7  9912

Unread = count of messages where created_at > last_read_at. Per-room counter cached in Redis, incremented when new messages arrive for a user who hasn't read them.

Deep dive 5: Delivery guarantees

WebSockets can drop. A message must not be lost.

You don't need full Paxos here. Order within a room is what matters, and a single partition per room gives you that for free.

Deep dive 6: Scaling out

Things to remember

Going deeper