Design a Chat Application
OOP design for a real-time chat application covering user management, direct and group messaging, message delivery states, online presence tracking, and chat history with read receipts.
The Problem
Your team builds the internal messaging tool for a 5,000-person company. The current system stores every message in a single database table sorted by timestamp. Last Monday, the CEO sent an urgent message to a 200-person group chat, and the system took 14 seconds to deliver it because the query scanned every message ever sent across every conversation. Meanwhile, three engineers reported that messages they sent showed as "delivered" even though the recipients never received them, because the delivery-tracking column was a single boolean with no per-recipient granularity.
Chat applications look simple on the surface, but the real design challenge lives in three areas: modeling conversations that scale from two-person DMs to large group chats, tracking message delivery states per recipient (sent, delivered, read), and maintaining online presence without drowning the system in heartbeat traffic. Get these wrong and you end up with phantom "delivered" badges that lie, group chats that slow to a crawl, and presence indicators that flicker between online and offline every few seconds.
Design the core classes for a chat application that handles user management, direct and group conversations, message sending with delivery tracking, read receipts, online presence, and message history retrieval.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "What types of conversations does the system support? Just one-on-one, or group chats too?"
Interviewer: "Both. Direct messages between two users, and group conversations with up to 500 members. Group chats need admin roles for managing membership."
Two conversation types with different membership rules. That tells us we need a Conversation abstraction with at least two concrete variants. Group conversations add admin-specific operations like adding and removing members.
You: "How should message delivery tracking work? Just a simple 'sent' flag, or more granular?"
Interviewer: "Three states: SENT when the server accepts the message, DELIVERED when the recipient's client acknowledges receipt, and READ when the recipient opens the conversation. For group chats, track these states per recipient."
Per-recipient delivery tracking in group chats is the core complexity. A message in a 200-person group needs 200 independent status records. This rules out a single status field on the Message object.
You: "How does online presence work? Do users explicitly set their status, or is it automatic?"
Interviewer: "Automatic heartbeat-based detection. Clients send a heartbeat every 30 seconds. If no heartbeat arrives within 60 seconds, mark the user as offline. Users can also manually set themselves as AWAY."
Good. Heartbeat-based presence means the system needs a timestamp-based check, not an explicit toggle. The 60-second timeout window is a concrete parameter we can design around.
You: "Should the system support different message types like images or file attachments, or just text?"
Interviewer: "Support text messages and file attachments. An attachment has a URL, file name, size, and MIME type. A message can have zero or more attachments."
That confirms messages are not just strings. We need an Attachment model, and the relationship is one-to-many (one message, many attachments).
You: "What about message editing and deletion? Can users modify messages after sending?"
Interviewer: "Out of scope for now. Once sent, a message is immutable. Focus on the core send/receive/read flow."
Good, that simplifies things. No edit history, no soft-delete flags. Messages are write-once.
You: "Are typing indicators in scope?"
Interviewer: "No. Focus on presence (online/offline/away) and message delivery states. Typing indicators are a nice-to-have extension."
You: "Is there any limit on message history retrieval? Should we support pagination?"
Interviewer: "Yes, load messages in pages of 50. Most recent first. Users should be able to scroll back through history."
That last question matters because unbounded history loading is what killed the original system. Pagination is a hard requirement.
Perfect. You have now clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
- Create and manage user accounts with display names and profile information
- Start direct (1-to-1) conversations and group conversations (up to 500 members)
- Send text messages with optional file attachments to any conversation
- Track message delivery status per recipient: SENT, DELIVERED, READ
- Track online presence via heartbeat with ONLINE, OFFLINE, and AWAY states
- Retrieve paginated message history for any conversation (50 messages per page)
- Manage group membership: add/remove members, assign/revoke admin roles
Non-Functional Requirements:
- Thread safety for concurrent message sending and status updates
- Extensibility for new message types, conversation features, and notification channels
- Efficient presence tracking that does not require scanning all users
Out of Scope:
- UI rendering and client-side logic
- Persistence and database layer
- Message editing or deletion
- Typing indicators
- End-to-end encryption
- Voice or video calls
- Push notification delivery mechanism
Example Inputs and Outputs
Scenario 1: Direct Message Flow
- Input: Alice sends "Hey, are you free for lunch?" to Bob
- Expected: Message is created with status SENT. When Bob's client connects and pulls messages, status updates to DELIVERED. When Bob opens the conversation, status updates to READ.
- Why: Validates the three-state delivery tracking on a simple 1-to-1 conversation.
Scenario 2: Group Message Delivery
- Input: Alice sends "Team standup in 5 minutes" to a group with Bob, Carol, and Dave. Bob is online, Carol is offline, Dave is away.
- Expected: Message status is SENT for all three. Bob's client acknowledges, so his status becomes DELIVERED. Carol's status stays SENT until she comes online. When Bob opens the chat, his status becomes READ while others remain unchanged.
- Why: Validates per-recipient delivery tracking in group conversations.
Scenario 3: Presence Tracking
- Input: Bob sends a heartbeat at 10:00:00. No further heartbeat arrives. System checks at 10:01:05.
- Expected: Bob's status transitions from ONLINE to OFFLINE because 65 seconds have passed since his last heartbeat, exceeding the 60-second timeout.
- Why: Validates heartbeat-based presence detection with configurable timeout.
Try It Yourself
Try it yourself
Before reading the solution, spend 20 minutes sketching a class diagram. Focus on how you would model the relationship between Conversation, Message, and per-recipient delivery status. The tricky part is not the message itself, but tracking delivery state independently for each participant. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look at the nouns in your requirements: user, conversation, message, attachment, delivery status, presence status. Each noun is a candidate entity, but not every noun deserves its own class. The key is deciding what has independent state and behavior.
A common mistake is lumping all message metadata (status, read timestamps, delivery timestamps) into the Message class itself. That works for direct messages, but falls apart in group chats where each recipient has independent delivery state. I see this in interviews constantly, and it forces a painful refactor mid-whiteboard when the candidate reaches the group chat scenario.
| Entity | Responsibility | Key attributes |
|---|---|---|
| User | Identity and profile. Owns no conversation logic. | userId, name, email, status |
| Conversation | Container for participants and messages. Knows its type but not delivery logic. | conversationId, participants, messages |
| DirectConversation | A conversation between exactly two users. No admin roles. | (inherits from Conversation) |
| GroupConversation | A conversation with 2-500 members. Has admins who can manage membership. | admins, groupName, maxMembers |
| Message | Immutable content unit. Knows its sender and conversation, but not per-recipient state. | messageId, senderId, content, timestamp, attachments |
| Attachment | File metadata. Value object attached to a message. | fileName, url, size, mimeType |
| MessageReceipt | Per-recipient delivery tracking. One per (message, recipient) pair. | recipientId, status, deliveredAt, readAt |
| MessageStatus | Enum for delivery state machine: SENT, DELIVERED, READ. | (enum values) |
| UserStatus | Enum for presence: ONLINE, OFFLINE, AWAY. | (enum values) |
| ChatService | Orchestrator. Routes messages, manages conversations, updates presence. | conversations, users, messageStore |
| MessageStore | Stores messages per conversation with pagination support. | messagesByConversation |
Notice we separated Message from MessageReceipt because they have fundamentally different lifecycles. A Message is created once and never changes. A MessageReceipt is created per recipient and updates multiple times as delivery progresses. Merging them would mean a group message with 200 recipients stores 200 copies of the same content, which is wasteful and violates normalization.
Step 2: Define Relationships and Class Design
Class Diagram
Deriving Class Interfaces
ChatService (The Orchestrator)
ChatService is the central coordinator. It owns all conversations and users, routes messages, and delegates delivery tracking. Nothing in the system happens without going through ChatService.
Deriving state from requirements:
| Requirement | What ChatService must track |
|---|---|
| "Create direct and group conversations" | All active conversations by ID |
| "Send messages to any conversation" | Users by ID (to validate sender) |
| "Track delivery status per recipient" | Receipts by message ID |
| "Track online presence" | Users with heartbeat timestamps |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Send text messages with attachments" | sendMessage(userId, conversationId, content, attachments) |
| "Start direct conversations" | createDirectConversation(user1, user2) |
| "Start group conversations" | createGroupConversation(name, creator, members) |
| "Track delivery: DELIVERED state" | markDelivered(messageId, recipientId) |
| "Track delivery: READ state" | markRead(conversationId, userId) |
| "Heartbeat-based presence" | updatePresence(userId) |
GroupConversation
GroupConversation extends Conversation with admin management. Only admins can add or remove members. The creator is automatically the first admin.
Deriving state from requirements:
| Requirement | What GroupConversation must track |
|---|---|
| "Group chats with admin roles" | Set of admin users |
| "Up to 500 members" | Maximum member count |
| "Group has a name" | Group display name |
Deriving methods from needs:
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.