Skip to main content

Chat Application

A messaging app that treats a one-to-one conversation and a group conversation as the same class, and treats "who's online" as a question separate from "who said what."

Requirements

Functional

  • Two users can start a direct chat and exchange text messages.
  • A group of users can share a chat room where every member sees every message.
  • A user's messages show up in order within a room, to everyone in it.
  • Other users can see whether a given user is currently online.

Non-functional

  • Adding a group chat feature must not require a separate code path from direct chat - a direct chat is just a room with two members.
  • Presence updates (online/offline) must reach members without those members polling for it on every message send.

Design

ChatRoom doesn't distinguish direct from group - it's a set of Users and an ordered list of Messages either way. A ChatService creates rooms and routes sends; a PresenceTracker owns online/offline state and notifies interested rooms, so a message send never has to ask "is this person online" itself.

SenderChatServiceChatRoomPresenceTrackersendMessage(roomId, text)1addMessage(message)2append to history3setOnline(user)4onStatusChange(user, ONLINE)5
  1. 1The sender only knows a room id - never whether the room is direct or group.
  2. 2The service looks up the room and appends the message; the room enforces membership.
  3. 3Every room keeps one ordered message list, regardless of member count.
  4. 4Presence is updated independently of any message being sent.
  5. 5The tracker calls back into every registered room; each room ignores the update unless the user is actually one of its members.

The only thing that varies between "direct" and "group" is how the room got created - two users versus a roster - never how a message flows through it once it exists.

Class diagram

«interface»PresenceObserver+ onStatusChange(user, status): voidChatService- rooms: Map<string, ChatRoom>+ createDirectChat(a, b): ChatRoom+ createGroupChat(users): ChatRoom+ sendMessage(roomId, sender, text): voidChatRoom- id: string- members: Set<User>- history: List<Message>+ addMessage(message): void+ addMember(user): void+ onStatusChange(user, status): voidUser- id: string- displayName: stringMessage- sender: User- text: string- sentAt: datetimePresenceTracker- status: Map<User, PresenceStatus>- observers: List<PresenceObserver>+ setOnline(user): void+ setOffline(user): void+ isOnline(user): bool+ addObserver(observer): void
implementsuses
ChatService creates ChatRooms and routes messages; PresenceTracker tracks status independently and pushes updates to interested rooms.

Code

import java.time.Instant;
import java.util.*;
 
class User {
final String id;
final String displayName;
 
User(String id, String displayName) {
this.id = id;
this.displayName = displayName;
}
}
 
class Message {
final User sender;
final String text;
final Instant sentAt;
 
Message(User sender, String text) {
this.sender = sender;
this.text = text;
this.sentAt = Instant.now();
}
}
 
enum PresenceStatus { ONLINE, OFFLINE }
 
interface PresenceObserver {
void onStatusChange(User user, PresenceStatus status);
}
 
class ChatRoom implements PresenceObserver {
final String id;
private final Set<User> members = new LinkedHashSet<>();
private final List<Message> history = new ArrayList<>();
 
ChatRoom(String id, Collection<User> initialMembers) {
this.id = id;
members.addAll(initialMembers);
}
 
void addMember(User user) {
members.add(user);
}
 
void addMessage(Message message) {
if (!members.contains(message.sender)) {
throw new IllegalStateException(message.sender.displayName + " is not a member of " + id);
}
history.add(message);
}
 
List<Message> getHistory() {
return Collections.unmodifiableList(history);
}
 
public void onStatusChange(User user, PresenceStatus status) {
if (!members.contains(user)) return;
System.out.println(user.displayName + " is now " + status + " in room " + id);
}
}
 
class PresenceTracker {
private final Map<String, PresenceStatus> status = new HashMap<>();
private final List<PresenceObserver> observers = new ArrayList<>();
 
void addObserver(PresenceObserver observer) {
observers.add(observer);
}
 
void setOnline(User user) {
status.put(user.id, PresenceStatus.ONLINE);
observers.forEach(o -> o.onStatusChange(user, PresenceStatus.ONLINE));
}
 
void setOffline(User user) {
status.put(user.id, PresenceStatus.OFFLINE);
observers.forEach(o -> o.onStatusChange(user, PresenceStatus.OFFLINE));
}
 
boolean isOnline(User user) {
return status.getOrDefault(user.id, PresenceStatus.OFFLINE) == PresenceStatus.ONLINE;
}
}
 
class ChatService {
private final Map<String, ChatRoom> rooms = new HashMap<>();
private final PresenceTracker presence = new PresenceTracker();
private int nextRoomId = 1;
 
ChatRoom createDirectChat(User a, User b) {
return createRoom(List.of(a, b));
}
 
ChatRoom createGroupChat(List<User> users) {
return createRoom(users);
}
 
private ChatRoom createRoom(List<User> users) {
String id = "room-" + (nextRoomId++);
ChatRoom room = new ChatRoom(id, users);
rooms.put(id, room);
presence.addObserver(room);
return room;
}
 
void sendMessage(String roomId, User sender, String text) {
ChatRoom room = rooms.get(roomId);
room.addMessage(new Message(sender, text));
}
}

Design decisions

  • No DirectChatRoom / GroupChatRoom subclasses. A direct chat is a ChatRoom with exactly two members; nothing about sending, ordering, or reading a message differs by member count, so a subclass hierarchy would exist only to enforce "exactly two," which a factory method (ChatService.createDirectChat) can do just as well without the extra types.
  • Presence is a separate tracker, not a field the room polls. If ChatRoom asked each member "are you online" on every send, presence and messaging would be coupled for no reason. PresenceTracker instead pushes status changes outward as events, so a room only reacts when something actually changes.
  • Message is immutable once created. A sent message never mutates - edits or deletes in a real system would be new events referencing the original message's id, not a change to the message object, which keeps message ordering and delivery guarantees simple to reason about.
  • What's missing for a real system: this models everything in-memory and synchronously; a production version would persist messages before acknowledging the sender, page a room's history instead of loading it whole, and give the client a way to know it's missed messages while offline (a per-user last-read cursor per room).
0%0 of 122 pages studied