Java Notes
Build a real-time Chat Application with Java WebSocket, Spring Boot, and STOMP protocol - private messaging, group chats, and online status.
Project Overview
The Real-Time Chat Application is a full-featured messaging platform built with Java Spring Boot and WebSocket technology. It supports one-on-one private messaging, group chats, online/offline status tracking, typing indicators, file sharing, and message persistence. The application uses STOMP over WebSocket for real-time bidirectional communication and MongoDB for flexible message storage.
This project is ideal for:
- Java developers wanting to learn WebSocket and real-time communication
- Students building impressive portfolio projects
- Developers preparing for interviews at messaging/social media companies
- Anyone wanting to understand pub/sub patterns and event-driven architecture
The system handles hundreds of concurrent connections with efficient message broadcasting, implements read receipts, supports emoji reactions, and provides a clean REST API for message history retrieval.
Features
- User Registration & Login — JWT-based authentication with refresh tokens
- Private Messaging — One-on-one real-time chat between any two users
- Group Chat — Create groups, add/remove members, admin controls
- Online/Offline Status — Real-time presence tracking with Redis
- Typing Indicators — Shows when other user is typing in real-time
- Read Receipts — Double-tick system (delivered ✓, read ✓✓)
- Message History — Paginated retrieval of past messages with search
- File Sharing — Send images, documents, and media files up to 10MB
- Emoji Reactions — React to messages with emoji (like, love, laugh, etc.)
- Message Editing — Edit sent messages within 15-minute window
- Message Deletion — Delete for self or delete for everyone
- Notification System — Push notifications for new messages when offline
- User Search — Find users by name, email, or username
- Chat Room Management — Create, rename, archive, and delete chat rooms
- Message Encryption — End-to-end encryption for private messages using AES-256
- Unread Count — Badge count for unread messages per conversation
- Last Seen — Shows last active time for offline users
- Pin Messages — Pin important messages in group chats
- Block Users — Block/unblock users to prevent messaging
- Rate Limiting — Prevent spam with message rate limiting per user
Project Structure
Database Schema (MongoDB Collections)
messages Collection
chatRooms Collection
users Collection
Redis Keys Structure
| online:users | SET of online user IDs |
| user:status:{userId} | HASH {status, lastSeen} |
| typing:{chatRoomId} | SET of currently typing user IDs (TTL: 3s) |
| unread:{userId}:{chatRoomId} | INT count of unread messages |
Key Code Snippets
WebSocket Configuration
package com.wohotech.chat.config;
import com.wohotech.chat.handler.WebSocketAuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final WebSocketAuthInterceptor authInterceptor;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// Enable a simple in-memory broker for subscriptions
// /topic for broadcast (group messages)
// /queue for private (point-to-point) messages
config.enableSimpleBroker("/topic", "/queue");
// Prefix for messages FROM client TO server
config.setApplicationDestinationPrefixes("/app");
// Prefix for user-specific destinations
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// WebSocket endpoint with SockJS fallback
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// Add JWT authentication interceptor for WebSocket connections
registration.interceptors(authInterceptor);
}
}ChatMessage Model
package com.wohotech.chat.model;
import com.wohotech.chat.enums.MessageStatus;
import com.wohotech.chat.enums.MessageType;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Document(collection = "messages")
@CompoundIndex(name = "chatRoom_timestamp_idx",
def = "{'chatRoomId': 1, 'createdAt': -1}")
@Getter @Setter
@NoArgsConstructor @AllArgsConstructor
@Builder
public class ChatMessage {
@Id
private String id;
@Indexed
private String chatRoomId;
private String senderId;
private String senderName;
private String recipientId;
private String content;
@Builder.Default
private MessageType messageType = MessageType.TEXT;
@Builder.Default
private MessageStatus status = MessageStatus.SENT;
@Builder.Default
private List<Reaction> reactions = new ArrayList<>();
private String replyTo; // Message ID being replied to
private FileAttachment attachment;
@Builder.Default
private boolean edited = false;
private LocalDateTime editedAt;
@Builder.Default
private List<String> deletedForUsers = new ArrayList<>();
@Builder.Default
private boolean deletedForEveryone = false;
@Builder.Default
private boolean pinned = false;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Reaction {
private String userId;
private String emoji;
private LocalDateTime timestamp;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class FileAttachment {
private String fileName;
private String fileUrl;
private long fileSize;
private String mimeType;
}
}Chat Service Implementation
package com.wohotech.chat.service;
import com.wohotech.chat.dto.ChatMessageDTO;
import com.wohotech.chat.enums.MessageStatus;
import com.wohotech.chat.enums.MessageType;
import com.wohotech.chat.model.ChatMessage;
import com.wohotech.chat.model.ChatRoom;
import com.wohotech.chat.repository.ChatMessageRepository;
import com.wohotech.chat.repository.ChatRoomRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class ChatServiceImpl implements ChatService {
private final ChatMessageRepository messageRepository;
private final ChatRoomRepository chatRoomRepository;
private final SimpMessagingTemplate messagingTemplate;
private final UserPresenceService presenceService;
private final NotificationService notificationService;
private final EncryptionService encryptionService;
@Override
public ChatMessage sendPrivateMessage(ChatMessageDTO messageDTO) {
log.info("Sending private message from {} to {}",
messageDTO.getSenderId(), messageDTO.getRecipientId());
// Get or create chat room
String chatRoomId = getChatRoomId(
messageDTO.getSenderId(), messageDTO.getRecipientId());
// Encrypt message content for private chats
String encryptedContent = encryptionService.encrypt(messageDTO.getContent());
ChatMessage message = ChatMessage.builder()
.chatRoomId(chatRoomId)
.senderId(messageDTO.getSenderId())
.senderName(messageDTO.getSenderName())
.recipientId(messageDTO.getRecipientId())
.content(encryptedContent)
.messageType(MessageType.TEXT)
.status(MessageStatus.SENT)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
ChatMessage saved = messageRepository.save(message);
// Send to recipient via WebSocket
messagingTemplate.convertAndSendToUser(
messageDTO.getRecipientId(),
"/queue/messages",
decryptForDelivery(saved)
);
// Update message status based on recipient's online status
if (presenceService.isUserOnline(messageDTO.getRecipientId())) {
saved.setStatus(MessageStatus.DELIVERED);
messageRepository.save(saved);
} else {
// Send push notification to offline user
notificationService.sendPushNotification(
messageDTO.getRecipientId(),
messageDTO.getSenderName() + ": " + messageDTO.getContent()
);
}
// Update last message in chat room
updateChatRoomLastMessage(chatRoomId, saved);
return saved;
}
@Override
public ChatMessage sendGroupMessage(ChatMessageDTO messageDTO, String groupId) {
log.info("Sending group message to group: {}", groupId);
ChatRoom group = chatRoomRepository.findByChatRoomId(groupId)
.orElseThrow(() -> new RuntimeException("Group not found: " + groupId));
ChatMessage message = ChatMessage.builder()
.chatRoomId(groupId)
.senderId(messageDTO.getSenderId())
.senderName(messageDTO.getSenderName())
.content(messageDTO.getContent()) // Group messages not encrypted
.messageType(MessageType.TEXT)
.status(MessageStatus.SENT)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
ChatMessage saved = messageRepository.save(message);
// Broadcast to all group members via topic
messagingTemplate.convertAndSend(
"/topic/group/" + groupId,
saved
);
// Send notifications to offline members
group.getMembers().stream()
.filter(member -> !member.getUserId().equals(messageDTO.getSenderId()))
.filter(member -> !presenceService.isUserOnline(member.getUserId()))
.forEach(member -> notificationService.sendPushNotification(
member.getUserId(),
group.getName() + " - " + messageDTO.getSenderName() +
": " + messageDTO.getContent()
));
updateChatRoomLastMessage(groupId, saved);
return saved;
}
@Override
public List<ChatMessage> getChatHistory(String chatRoomId, int page, int size) {
PageRequest pageRequest = PageRequest.of(page, size,
Sort.by(Sort.Direction.DESC, "createdAt"));
List<ChatMessage> messages = messageRepository
.findByChatRoomId(chatRoomId, pageRequest);
// Decrypt private messages before returning
return messages.stream()
.map(this::decryptForDelivery)
.toList();
}
@Override
public void markAsRead(String chatRoomId, String userId) {
List<ChatMessage> unreadMessages = messageRepository
.findByChatRoomIdAndRecipientIdAndStatus(
chatRoomId, userId, MessageStatus.DELIVERED);
unreadMessages.forEach(msg -> {
msg.setStatus(MessageStatus.READ);
msg.setUpdatedAt(LocalDateTime.now());
});
messageRepository.saveAll(unreadMessages);
// Notify sender about read receipt
if (!unreadMessages.isEmpty()) {
String senderId = unreadMessages.get(0).getSenderId();
messagingTemplate.convertAndSendToUser(
senderId,
"/queue/read-receipt",
Map.of("chatRoomId", chatRoomId, "readBy", userId,
"timestamp", LocalDateTime.now())
);
}
}
@Override
public void handleTypingStatus(String chatRoomId, String userId, boolean isTyping) {
messagingTemplate.convertAndSend(
"/topic/typing/" + chatRoomId,
Map.of("userId", userId, "isTyping", isTyping)
);
}
private String getChatRoomId(String userId1, String userId2) {
// Ensure consistent room ID regardless of who initiates
String sortedId = userId1.compareTo(userId2) < 0
? userId1 + "_" + userId2
: userId2 + "_" + userId1;
chatRoomRepository.findByChatRoomId(sortedId).orElseGet(() -> {
ChatRoom room = ChatRoom.builder()
.chatRoomId(sortedId)
.type(ChatRoomType.PRIVATE)
.members(List.of(
new ChatRoom.Member(userId1, "MEMBER", LocalDateTime.now()),
new ChatRoom.Member(userId2, "MEMBER", LocalDateTime.now())
))
.createdAt(LocalDateTime.now())
.build();
return chatRoomRepository.save(room);
});
return sortedId;
}
private ChatMessage decryptForDelivery(ChatMessage message) {
if (message.getChatRoomId().contains("_")) { // Private chat
message.setContent(encryptionService.decrypt(message.getContent()));
}
return message;
}
private void updateChatRoomLastMessage(String chatRoomId, ChatMessage message) {
chatRoomRepository.findByChatRoomId(chatRoomId).ifPresent(room -> {
room.setLastMessage(new ChatRoom.LastMessage(
message.getContent(), message.getSenderId(), message.getCreatedAt()));
room.setUpdatedAt(LocalDateTime.now());
chatRoomRepository.save(room);
});
}
}Chat Controller (WebSocket Message Mapping)
package com.wohotech.chat.controller;
import com.wohotech.chat.dto.ChatMessageDTO;
import com.wohotech.chat.dto.TypingStatusDTO;
import com.wohotech.chat.model.ChatMessage;
import com.wohotech.chat.service.ChatService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.*;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
@Controller
@RequiredArgsConstructor
@Slf4j
public class ChatController {
private final ChatService chatService;
/**
* Handle private messages
* Client sends to: /app/chat.sendPrivate
* Recipient receives on: /user/{recipientId}/queue/messages
*/
@MessageMapping("/chat.sendPrivate")
public void sendPrivateMessage(@Payload ChatMessageDTO messageDTO,
SimpMessageHeaderAccessor headerAccessor) {
String userId = headerAccessor.getUser().getName();
messageDTO.setSenderId(userId);
log.info("Private message from {} to {}: {}",
userId, messageDTO.getRecipientId(),
messageDTO.getContent().substring(0, Math.min(50, messageDTO.getContent().length())));
chatService.sendPrivateMessage(messageDTO);
}
/**
* Handle group messages
* Client sends to: /app/chat.sendGroup/{groupId}
* All members receive on: /topic/group/{groupId}
*/
@MessageMapping("/chat.sendGroup/{groupId}")
public void sendGroupMessage(@Payload ChatMessageDTO messageDTO,
@DestinationVariable String groupId,
SimpMessageHeaderAccessor headerAccessor) {
String userId = headerAccessor.getUser().getName();
messageDTO.setSenderId(userId);
chatService.sendGroupMessage(messageDTO, groupId);
}
/**
* Handle typing indicators
* Client sends to: /app/chat.typing
* Others in chat receive on: /topic/typing/{chatRoomId}
*/
@MessageMapping("/chat.typing")
public void handleTyping(@Payload TypingStatusDTO typingStatus,
SimpMessageHeaderAccessor headerAccessor) {
String userId = headerAccessor.getUser().getName();
chatService.handleTypingStatus(
typingStatus.getChatRoomId(), userId, typingStatus.isTyping());
}
/**
* Handle read receipts
* Client sends to: /app/chat.markRead
*/
@MessageMapping("/chat.markRead")
public void markMessagesAsRead(@Payload Map<String, String> payload,
SimpMessageHeaderAccessor headerAccessor) {
String userId = headerAccessor.getUser().getName();
String chatRoomId = payload.get("chatRoomId");
chatService.markAsRead(chatRoomId, userId);
}
}WebSocket Event Listener (Online/Offline Tracking)
package com.wohotech.chat.event;
import com.wohotech.chat.service.UserPresenceService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import java.time.LocalDateTime;
import java.util.Map;
@Component
@RequiredArgsConstructor
@Slf4j
public class WebSocketEventListener {
private final UserPresenceService presenceService;
private final SimpMessagingTemplate messagingTemplate;
@EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String userId = headerAccessor.getUser().getName();
log.info("User connected: {}", userId);
// Mark user as online in Redis
presenceService.setUserOnline(userId);
// Broadcast online status to all subscribers
messagingTemplate.convertAndSend("/topic/presence",
Map.of("userId", userId, "status", "ONLINE",
"timestamp", LocalDateTime.now()));
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String userId = headerAccessor.getUser().getName();
log.info("User disconnected: {}", userId);
// Mark user as offline and update last seen
presenceService.setUserOffline(userId);
// Broadcast offline status
messagingTemplate.convertAndSend("/topic/presence",
Map.of("userId", userId, "status", "OFFLINE",
"lastSeen", LocalDateTime.now()));
}
}Message Repository
package com.wohotech.chat.repository;
import com.wohotech.chat.enums.MessageStatus;
import com.wohotech.chat.model.ChatMessage;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface ChatMessageRepository extends MongoRepository<ChatMessage, String> {
List<ChatMessage> findByChatRoomId(String chatRoomId, Pageable pageable);
List<ChatMessage> findByChatRoomIdAndRecipientIdAndStatus(
String chatRoomId, String recipientId, MessageStatus status);
@Query("{'chatRoomId': ?0, 'recipientId': ?1, 'status': {$ne: 'READ'}}")
long countUnreadMessages(String chatRoomId, String userId);
@Query("{'chatRoomId': ?0, 'content': {$regex: ?1, $options: 'i'}}")
List<ChatMessage> searchMessages(String chatRoomId, String keyword, Pageable pageable);
List<ChatMessage> findByChatRoomIdAndCreatedAtBetween(
String chatRoomId, LocalDateTime start, LocalDateTime end);
void deleteByChatRoomIdAndCreatedAtBefore(String chatRoomId, LocalDateTime before);
}API Endpoints
REST APIs (HTTP)
| Method | URL | Description | Auth |
|---|---|---|---|
| POST | /api/v1/auth/register | Register new user | No |
| POST | /api/v1/auth/login | Login and get JWT | No |
| GET | /api/v1/users/search?q={query} | Search users by name/email | Yes |
| GET | /api/v1/users/{userId}/profile | Get user profile | Yes |
| PUT | /api/v1/users/profile | Update own profile | Yes |
| GET | /api/v1/chat/rooms | Get all chat rooms for user | Yes |
| POST | /api/v1/chat/groups | Create a new group | Yes |
| PUT | /api/v1/chat/groups/{groupId} | Update group info | Yes (Admin) |
| POST | /api/v1/chat/groups/{groupId}/members | Add member to group | Yes (Admin) |
| DELETE | /api/v1/chat/groups/{groupId}/members/{userId} | Remove member | Yes (Admin) |
| GET | /api/v1/chat/{chatRoomId}/messages?page=0&size=50 | Get message history | Yes |
| GET | /api/v1/chat/{chatRoomId}/messages/search?q={keyword} | Search messages | Yes |
| POST | /api/v1/chat/messages/{messageId}/react | Add reaction to message | Yes |
| PUT | /api/v1/chat/messages/{messageId} | Edit message | Yes (Sender) |
| DELETE | /api/v1/chat/messages/{messageId} | Delete message | Yes (Sender) |
| POST | /api/v1/files/upload | Upload file attachment | Yes |
| POST | /api/v1/users/{userId}/block | Block a user | Yes |
WebSocket Endpoints (STOMP)
| Direction | Destination | Description |
|---|---|---|
| Client → Server | /app/chat.sendPrivate | Send private message |
| Client → Server | /app/chat.sendGroup/{groupId} | Send group message |
| Client → Server | /app/chat.typing | Send typing indicator |
| Client → Server | /app/chat.markRead | Mark messages as read |
| Server → Client | /user/{userId}/queue/messages | Receive private messages |
| Server → Client | /user/{userId}/queue/read-receipt | Receive read receipts |
| Server → Client | /topic/group/{groupId} | Receive group messages |
| Server → Client | /topic/typing/{chatRoomId} | Receive typing indicators |
| Server → Client | /topic/presence | Receive online/offline updates |
Sample WebSocket Connection (JavaScript Client)
How to Run
Prerequisites
- Java 17+
- MongoDB 7.0 (local or Atlas)
- Redis 7.x
- Maven 3.9+
- Docker & Docker Compose (optional)
Option 1: Docker Compose (Recommended)
git clone https://github.com/wohotech/chat-application.git
cd chat-application
docker-compose up -ddocker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- mongodb
- redis
environment:
SPRING_DATA_MONGODB_URI: mongodb://mongodb:27017/chatdb
SPRING_REDIS_HOST: redis
mongodb:
image: mongo:7.0
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
mongo_data:Option 2: Manual Setup
Step 1: Install and start MongoDB
mongod --dbpath /data/chatdbStep 2: Install and start Redis
redis-serverStep 3: Configure application.yml
Step 4: Build and run
mvn clean install -DskipTests
mvn spring-boot:runStep 5: Access the application
- WebSocket Endpoint:
ws://localhost:8080/ws - REST API:
http://localhost:8080/api/v1 - Test Client:
http://localhost:8080/index.html
Screenshots Description
- Login Screen — Clean login form with JWT token response in dev tools
- Chat List — All conversations with last message preview, unread badges, and online indicators
- Private Chat — Real-time messaging with typing indicator, read receipts (✓✓), and timestamps
- Group Chat — Multiple users chatting with member list sidebar and admin controls
- File Sharing — Image preview inline, document download link, and upload progress
- Online Status — Green dot for online, gray for offline with "Last seen" text
- WebSocket Inspector — Chrome DevTools showing STOMP frames being exchanged
- MongoDB Collections — Compass view showing messages collection with nested documents
Future Enhancements
- Voice Messages — Record and send audio messages with waveform visualization
- Video Calling — WebRTC integration for peer-to-peer video calls
- Screen Sharing — Share screen during video calls
- Message Forwarding — Forward messages to other chats
- Disappearing Messages — Auto-delete after 24 hours option
- Chat Backup — Export chat history as PDF or JSON
- Bot Integration — Chatbot support for automated responses
- Multi-device Sync — Sync messages across all logged-in devices
- Horizontal Scaling — Redis Pub/Sub for multi-instance WebSocket servers
- AI Features — Smart reply suggestions, message summarization
Interview Questions
Q1: Why did you choose WebSocket over HTTP polling for real-time messaging?
Answer: WebSocket provides full-duplex communication over a single TCP connection, meaning both server and client can send data at any time without the overhead of repeated HTTP handshakes. HTTP polling wastes bandwidth with empty responses, long-polling holds connections open inefficiently, and SSE is unidirectional. WebSocket latency is ~2ms vs ~100ms for polling. For a chat app with typing indicators and presence updates, WebSocket is the only practical choice.
Q2: What is STOMP and why use it over raw WebSocket?
Answer: STOMP (Simple Text Oriented Messaging Protocol) is a messaging protocol that works over WebSocket. Raw WebSocket only provides a byte stream — you'd need to build your own routing, subscription, and message format handling. STOMP provides: (1) Destinations for routing (/topic/, /queue/), (2) Subscribe/unsubscribe semantics, (3) Message headers for metadata, (4) Built-in support in Spring's SimpMessagingTemplate. It's like having HTTP conventions but for WebSocket.
Q3: How do you handle scaling WebSocket connections across multiple server instances?
Answer: In a single-instance setup, the in-memory STOMP broker works fine. For multiple instances: (1) Replace the simple broker with a full message broker (RabbitMQ/Redis Pub/Sub), (2) Use Spring's @EnableWebSocketMessageBrokerExternal with relay configuration, (3) Each server instance subscribes to the message broker, (4) When User A on Server 1 sends a message to User B on Server 2, it flows through the broker. We use Redis Pub/Sub in this project for the presence layer.
Q4: How do you ensure messages aren't lost when a user is offline?
Answer: Messages are always persisted to MongoDB first, regardless of online status. When the recipient comes online: (1) Their client fetches unread messages via REST API, (2) The WebSocket event listener updates their status and triggers delivery of queued messages, (3) Push notifications alert them even when the app is closed. The unread count in Redis provides instant badge counts without querying MongoDB.
Q5: Explain the message delivery status flow (Sent → Delivered → Read).
Answer: SENT — Message saved to DB and acknowledged by server. DELIVERED — Recipient's WebSocket connection received the message (checked via presence service). If offline, stays at SENT until they connect. READ — Recipient explicitly called the markAsRead endpoint (triggered when they view the chat). Each status change sends a real-time update back to the sender via /queue/read-receipt.
Q6: How did you implement end-to-end encryption?
Answer: For private chats, we use AES-256 symmetric encryption. A shared secret key is derived from both users' public keys using Diffie-Hellman key exchange during first contact. Messages are encrypted before MongoDB storage and decrypted only on retrieval for the intended recipient. The server never stores plaintext private messages. Group messages use transport-layer encryption (TLS) but aren't E2E encrypted since key management for groups is complex.
Key Learnings
- WebSocket lifecycle management and connection handling
- STOMP protocol message routing and subscription patterns
- MongoDB document design for flexible, nested data structures
- Redis for real-time presence tracking and pub/sub messaging
- Event-driven architecture with Spring's event listeners
- Handling concurrent WebSocket connections efficiently
- Message delivery guarantees and idempotency
- Building scalable real-time systems with proper fallback mechanisms
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Chat Application.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, projects, chat, application
Related Java Master Course Topics