Design WhatsApp (LLD)
Users, chats, messages, delivery/read receipts, and group management — the messaging object model.
Problem Statement
Design the object model for WhatsApp from an LLD/OOD perspective. The system supports registered users, one-to-one chats, group chats, text messages, group membership, and message receipts that move through Sent -> Delivered -> Read.
The core interview challenge is not mobile networking or storage scale. It is placing coordination in a Mediator (ChatServer), keeping chat objects responsible for membership, and modelling the delivery lifecycle so invalid transitions are impossible.
Business context
Messaging systems are a favorite Meta, Google, and Amazon LLD topic because they combine familiar product behavior with subtle object boundaries. A strong solution separates user identity, chat membership, routing, message state, and notification concerns.
Interviewers want to see whether the candidate avoids turning User into a god object, whether ChatServer coordinates without owning every policy, and whether receipts are represented as a lifecycle rather than a set of loose booleans.
Functional Requirements
Register users with stable ids and display names.
Create one-to-one chats between exactly two distinct users.
Create group chats with multiple users and allow membership changes.
Send text messages from a chat member to the other participants.
Route each message through a mediator instead of direct user-to-user calls.
Track message status through Sent -> Delivered -> Read and reject invalid transitions.
Notify interested observers when a message status changes.
Allow a recipient to mark a delivered message as read.
Non-Functional Requirements
Correct lifecycle semantics
A message must not jump from Sent to Read without first becoming Delivered. The state object owns that rule.
Loose coupling between users
User objects should not call each other directly. ChatServer is the mediator that validates membership and routes messages.
Extensible delivery policy
Online-only delivery is the default, but retry queues, device fanout, or priority rules should be replaceable as strategies.
Notification fanout
Receipt updates should be pushed through observers so the message lifecycle does not depend on a specific UI, socket, or analytics sink.
Encapsulation of group rules
Group membership checks and membership mutations belong in GroupChat, not scattered across every send path.
Requirement Clarification
QAre we designing encryption, media upload, and mobile sync?
No. Focus on the LLD object model for users, chats, messages, routing, membership, and receipt state. Encryption and media storage are boundary services.
QIs message status per chat or per recipient?
The base design uses an aggregate status for clarity. In production, store per-recipient receipts and derive aggregate Delivered and Read from them.
QCan a sender message a chat they do not belong to?
No. The mediator resolves the chat and asks the chat to validate membership before creating or routing the message.
QWhat happens when a recipient is offline?
The delivery strategy decides. The default strategy delivers only to online users; an offline recipient remains a production extension through queues and device fanout.
QDo group admins and roles matter in the core design?
Not for the first pass. Model add/remove membership on GroupChat, then mention roles as an extension.
UML Class Diagram
Sequence Diagram
Entity Identification
User
Represents an account participating in chats. It can receive routed messages and observe status updates, but it does not locate or contact other users directly.
Message
Immutable message identity, chat id, sender, text, timestamp, and mutable delivery state. It notifies observers after successful lifecycle transitions.
MessageStatus / MessageState
Encodes the Sent -> Delivered -> Read lifecycle. State objects decide whether a transition is allowed, avoiding scattered if-else checks.
StatusObserver
Observer contract for receipt updates. Users, mobile sessions, analytics, or notification adapters can subscribe without changing Message.
Chat
Abstract conversation boundary. It stores message history and exposes membership-aware recipient lookup while subclasses enforce participant rules.
OneToOneChat
A chat with exactly two distinct users. It rejects non-members and computes the single recipient for a sender.
GroupChat
A chat with a mutable set of members. It centralizes add, remove, and membership validation for group conversations.
ChatServer
Mediator and aggregate service. It registers users, creates chats, routes messages, records messages, applies delivery policy, and handles read receipts.
DeliveryPolicy
Strategy for deciding whether a recipient should receive a message immediately. The demo uses online-only delivery.
Design Patterns Used
ChatServer coordinates user lookup, chat lookup, membership validation, routing, storage of message references, and receipt actions. Users remain independent participants rather than directly depending on each other.
Message publishes status transitions to StatusObserver instances. UI sessions, sender devices, analytics, and notifications can react without being hard-coded into the message state machine.
MessageState implementations encode which transition is legal from Sent, Delivered, and Read. This makes the lifecycle explicit and prevents invalid boolean combinations.
DeliveryPolicy decides whether to deliver now. Online-only delivery, durable queues, priority fanout, or retry-aware delivery can be swapped without editing ChatServer.sendMessage.
Step-by-Step Design
1Put routing behind a mediator
Make ChatServer the only object that resolves users and chats. A sender asks the server to send; the server validates membership, creates the message, records it, and routes to recipients.
public Message sendMessage(String chatId, String senderId, String text) { Chat chat = chat(chatId); User sender = user(senderId); chat.requireMember(senderId); Message message = new Message(nextMessageId(), chatId, sender, text); chat.record(message); return message; }2Make chats own participant rules
OneToOneChat enforces exactly two users. GroupChat owns its member map and exposes add/remove operations. The server asks the chat for recipients instead of duplicating rules.
3Represent receipts as a state machine
The lifecycle is Sent -> Delivered -> Read. A SentState can deliver, a DeliveredState can read, and ReadState is terminal.
interface MessageState { MessageStatus status(); MessageState deliver(); MessageState read(); }4Notify observers after successful transitions
Message.transition captures the previous status, installs the next state, and notifies observers. Failed transitions throw before any observer sees a false update.
private void transition(MessageState next) { MessageStatus oldStatus = status(); if (oldStatus == next.status()) { return; } state = next; notifyObservers(oldStatus, status()); }5Keep delivery decisions swappable
The first policy is simple: deliver only to online recipients. In a real system this policy can enqueue offline delivery, fan out to multiple devices, or apply priority without changing chat membership logic.
6Store enough references for read receipts
ChatServer keeps a message map so markRead can find the message, validate the reader against the owning chat, and ask Message to move from Delivered to Read.
Complete Java Implementation
Explanation of Every Class
User
Represents a participant and implements StatusObserver. It receives routed messages and receipt updates, but it never finds recipients or writes to another user directly.
Message
Carries message identity, chat id, sender, text, creation time, observers, and the current MessageState. It exposes markDelivered and markRead as the only lifecycle mutation methods.
MessageStatus / MessageState
MessageStatus names the visible states. SentState, DeliveredState, and ReadState encode allowed transitions so Read cannot happen before Delivered.
StatusObserver
Small observer interface used by Message to publish status transitions. The demo uses users as observers; production can attach sessions, web sockets, metrics, or notifications.
Chat
Abstract conversation base. It stores message history and provides recipientsFor, while forcing subclasses to define participant lookup and membership validation.
OneToOneChat
Concrete chat with exactly two distinct users. It keeps the one-to-one invariant local and rejects any operation from a non-member.
GroupChat
Concrete chat backed by a member map. It centralizes group add, remove, participant listing, and membership validation.
ChatServer
The mediator. It owns registries of users, chats, and messages; creates chats; validates senders and readers; routes messages; and triggers delivery/read status transitions.
DeliveryPolicy / OnlineDeliveryPolicy
Strategy seam for delivery decisions. OnlineDeliveryPolicy is the simple demo implementation; queue-backed or device-aware policies can replace it.
Main
Executable walkthrough that registers users, creates one-to-one and group chats, sends messages, marks one as read, and prints resulting state.
Dry Run
Sample input
Users Alice, Bob, and Charlie are registered. Chat c1 is Alice/Bob. Group g1 contains all three. All users are online.
| Step | Action | Mediator decision | Recipients | Message status | Observer update |
|---|---|---|---|---|---|
| 1 | Alice sends 'Hi Bob' in c1 | Validate Alice belongs to c1 and create m-1 | Bob | Sent -> Delivered | Alice and Bob receive Delivered receipt |
| 2 | Bob marks m-1 as read | Validate Bob belongs to c1 and is not sender | None | Delivered -> Read | Alice and Bob receive Read receipt |
| 3 | Bob sends 'Dinner at 8' in g1 | Validate Bob belongs to g1 and record m-2 | Alice, Charlie | Sent -> Delivered | All group observers receive Delivered receipt |
| 4 | Charlie goes offline, Alice sends again | DeliveryPolicy skips Charlie and delivers to Bob | Bob | Sent -> Delivered | Observers see aggregate Delivered; per-user pending is an extension |
The dry run shows the mediator doing validation and routing, while Message alone changes status. Step 4 highlights the simplification in this LLD: aggregate status is easy to explain, and per-recipient receipt rows are the production extension.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| registerUser | O(1) | O(1) | Hash-map insertion by user id. |
| createOneToOneChat | O(1) | O(1) | Lookup two users and store one chat object. |
| createGroupChat | O(M) | O(M) | M initial members are resolved and copied into the group member map. |
| sendMessage | O(P) | O(P) | P participants are scanned for observers and recipients. Message creation and maps are constant time. |
| markRead | O(1) | O(1) | Lookup message and chat, validate membership by map for groups, then transition state. |
| history | O(1) | O(1) | The sample returns an unmodifiable view. A defensive copy would be O(H). |
For interview LLD, P is participants in a chat and M is group members at creation. At WhatsApp scale, routing becomes asynchronous fanout and receipt updates become per-recipient records, but the same object seams remain useful.
Extensibility
Per-recipient receipts
Replace the aggregate MessageState with a map from user id to receipt state, then derive chat-level status as all delivered or all read.
Offline delivery queue
Add a queue-backed DeliveryPolicy or DeliveryStrategy that stores pending deliveries and retries when a user reconnects.
Group roles
Extend GroupChat membership values from User to GroupMember with role and joinedAt fields. Add admin checks inside add/remove operations.
Media messages
Introduce a MessageContent strategy or hierarchy for text, image, audio, and document payloads while keeping routing unchanged.
Multiple devices per user
Let User own devices or sessions, and make delivery fan out to active devices through observers or adapters.
Alternative Designs
Direct user-to-user messaging
Let User.sendTo(User, text) call the recipient directly and update receipts locally.
Tradeoffs
Simple demo code, but it couples users, duplicates membership checks, makes group routing awkward, and hides the mediator boundary interviewers expect.
Event bus instead of direct observers
Publish MessageStatusChanged events to a bus and let UI, notification, and analytics consumers subscribe independently.
Tradeoffs
Closer to production and easier to scale, but more infrastructure than necessary for a focused LLD unless the interviewer asks for async processing.
Per-recipient receipt aggregate
Model MessageReceipt objects for every recipient and store status, deliveredAt, and readAt per user.
Tradeoffs
More accurate for groups and offline users, but adds storage and aggregation complexity. Use it as a follow-up after the base state machine is clear.
Common Mistakes
- ×
Letting User directly call other User objects, which removes the mediator and spreads routing rules everywhere.
- ×
Representing status with booleans such as isSent, isDelivered, and isRead, which permits impossible combinations.
- ×
Skipping membership validation before sending or marking a message read.
- ×
Putting group add/remove logic in ChatServer instead of inside GroupChat.
- ×
Hard-coding online-only delivery inside message state rather than behind a strategy.
- ×
Notifying observers before the state transition succeeds.
- ×
Treating group read receipts as a single boolean without explaining per-recipient receipt extension.
Follow-up Interview Questions
QHow would you support per-recipient delivery and read receipts in groups?
Create MessageReceipt per recipient with status, deliveredAt, and readAt. The message can derive aggregate state from the receipt collection while individual users see precise receipt status.
QHow do offline users eventually receive messages?
Replace OnlineDeliveryPolicy with a queue-backed strategy. The mediator writes pending deliveries, and reconnect logic drains the queue through the same receive path.
QWhere would end-to-end encryption fit?
As a boundary service before message creation and after delivery. The mediator should route encrypted payloads without needing to inspect clear text.
QHow do you prevent non-members from reading group history?
Keep Chat.requireMember on all read paths, not only send paths. For historical access after removal, add membership intervals and check whether the user was a member at message time.
QHow would you make status notifications reliable?
Persist status changes and publish them through an event bus or outbox. Observers become adapters consuming durable events instead of in-memory callbacks.
Production Considerations
Durable message store
Persist messages, chat membership, and receipts. The in-memory maps become repositories or DAO interfaces behind ChatServer.
Per-device fanout
A user may have phone, desktop, and web sessions. Delivery policy should fan out to devices and aggregate acknowledgements carefully.
Ordering and idempotency
Assign monotonic sequence numbers per chat and make send/read operations idempotent so retries do not duplicate messages or regress status.
Privacy and encryption
Keep encryption outside the domain routing model, but ensure the server never needs plaintext to validate membership or route messages.
Observability
Track send latency, delivery lag, read lag, observer failures, and queue depth. Receipt lifecycle metrics are essential for debugging user-visible delays.
What Interviewers Look For
Did the candidate make ChatServer a mediator instead of direct user coupling?
Is the Sent -> Delivered -> Read lifecycle explicit and protected from invalid transitions?
Are one-to-one and group membership rules owned by the chat classes?
Can delivery policy change without rewriting the message or chat models?
Are observers used for status updates without mixing UI concerns into Message?
Does the candidate acknowledge per-recipient receipts as the realistic group-chat extension?
Quiz
0/5 answered
1.Why is **ChatServer** modelled as a Mediator?
2.What problem does the State pattern solve for message receipts?
3.Which class should own group add/remove membership rules?
4.Why is **DeliveryPolicy** a Strategy?
5.In a production group chat, what is the usual improvement over one aggregate message status?
Practice Variants
Add per-recipient receipts
AdvancedIntroduce MessageReceipt with recipient, status, deliveredAt, and readAt. Update the mediator to transition a single recipient receipt at a time.
Support group admins
IntermediateChange group membership from user map to member-role map. Enforce that only admins can add or remove members.
Queue offline delivery
ExpertReplace OnlineDeliveryPolicy with a strategy that enqueues undelivered messages and drains them when User.setOnline(true) is observed.
Flashcards
Cheat Sheet
Entities: User, Message, MessageStatus/MessageState, StatusObserver, Chat, OneToOneChat, GroupChat, ChatServer, DeliveryPolicy.
Patterns: Mediator for routing, Observer for status updates, State for Sent -> Delivered -> Read, Strategy for delivery policy.
Core flow: sender calls ChatServer.sendMessage → server validates membership → creates Message in Sent → records in Chat → routes recipients through DeliveryPolicy → marks Delivered → observer notifications fire.
Read flow: recipient calls markRead → server validates membership and non-sender → MessageState moves Delivered to Read → observers receive update.
Invariants: no direct user-to-user routing; sender must be chat member; read cannot precede delivered; group membership rules stay in GroupChat.
Complexity: send is O(P) for participants, group creation is O(M), read is O(1) in the in-memory model.
Extend: per-recipient receipts, offline queues, group roles, media content, multi-device fanout.
References
- BookDesign Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides
- BookEnterprise Integration Patterns — Gregor Hohpe and Bobby Woolf
- DocsRefactoring Guru — Mediator
- DocsRefactoring Guru — Observer