97. Case Study: A Real-Time Chat Service
97. Case Study: A Real-Time Chat Service
As an engineer, the challenge of building a real-time chat service is always an interesting one to explore. Delivering low-latency two-way communication along with scalability and adequate security is a complex combination, yet a deeply fundamental one in today’s digital era. In this article, I’ll break down a case study on building a real-time chat service with WebSocket using Node.js and React, complete with code samples, simulations, and illustrations of the data flow. I hope you find it useful!
1. Background: Why Real-Time Chat?
These days, real-time chat services are everywhere: from messaging apps and customer support to live comment features on event streaming platforms. Real-time solutions have become a key driver of productivity and user engagement. In this case, we want to build a simple chat service, a group chat, with the following features:
- Each user can join one or more rooms
- Messages are sent and received in real time
- Chat history is stored
- Handling user join/leave notifications
2. Architecture in Brief
The architecture we’ll use is as follows:
- Client: React (WebSocket client)
- Server: Node.js with Express & ws (WebSocket library)
- Database: MongoDB (storage for chat history & user-room relations)
Architecture diagram:
flowchart LR
User1 --Socket--> Server
User2 --Socket--> Server
Server --REST(GET/POST)--> MongoDB
Server --Socket Broadcast--> User1
Server --Socket Broadcast--> User2
The server acts as a two-way “relay”: it handles socket connections from clients, forwards messages to other users in the same room, and persists chats to the database.
3. Data Schema Design
Here is an example MongoDB schema for the chat collection:
| Field | Type | Description |
|---|---|---|
| _id | ObjectId | Primary key |
| room_id | String | Room ID |
| sender | String | User who sent the message |
| message | String | Message content |
| created_at | ISODate | Timestamp when sent |
And the user-to-room relation (using the user_rooms collection):
| Field | Type | Description |
|---|---|---|
| _id | ObjectId | Primary key |
| user_id | String | User ID |
| room_id | String | Room ID |
4. Backend Implementation with Node.js
Let’s start with the WebSocket server.
1// index.js
2const express = require('express');
3const http = require('http');
4const WebSocket = require('ws');
5const { MongoClient } = require('mongodb');
6
7const app = express();
8const server = http.createServer(app);
9const wss = new WebSocket.Server({ server });
10
11// MongoDB connection
12const mongoClient = new MongoClient('mongodb://localhost:27017');
13let chatCollection;
14mongoClient.connect().then(client => {
15 const db = client.db('realtime_chat_demo');
16 chatCollection = db.collection('chats');
17});
18
19// Map user to ws
20const rooms = {};
21
22wss.on('connection', (ws) => {
23 ws.on('message', async (data) => {
24 const msg = JSON.parse(data);
25 if (msg.type === 'JOIN') {
26 ws.room = msg.room_id;
27 // Register socket to the room
28 rooms[ws.room] = rooms[ws.room] || [];
29 rooms[ws.room].push(ws);
30 ws.send(JSON.stringify({ type: 'NOTIF', text: `Welcome to room ${ws.room}` }));
31 }
32 else if (msg.type === 'CHAT') {
33 // Save to MongoDB
34 await chatCollection.insertOne({
35 room_id: ws.room,
36 sender: msg.sender,
37 message: msg.message,
38 created_at: new Date()
39 });
40 // Broadcast to all ws in that room
41 (rooms[ws.room] || []).forEach(client => {
42 if (client.readyState === WebSocket.OPEN) {
43 client.send(JSON.stringify({
44 type: 'CHAT',
45 sender: msg.sender,
46 message: msg.message,
47 room_id: ws.room
48 }));
49 }
50 });
51 }
52 });
53 ws.on('close', () => {
54 if (ws.room && rooms[ws.room]) {
55 rooms[ws.room] = rooms[ws.room].filter(client => client !== ws);
56 }
57 });
58});
59
60server.listen(9000, () => {
61 console.log('Listening on port 9000');
62});Quick Explanation
- When a client sends a
JOINtype, the server adds the socket to a specific room. - For a
CHATmessage, the server saves it to the database and forwards the message to all sockets in that room. - The broadcast is done locally in the server’s memory (suitable for small to medium scale).
5. Client Side: React + WebSocket
A React component to connect and chat in real time.
1// ChatRoom.jsx
2import React, { useState, useEffect, useRef } from 'react';
3
4const WS_URL = 'ws://localhost:9000';
5
6export default function ChatRoom({ user, room_id }) {
7 const [messages, setMessages] = useState([]);
8 const [input, setInput] = useState('');
9 const ws = useRef(null);
10
11 useEffect(() => {
12 ws.current = new WebSocket(WS_URL);
13 ws.current.onopen = () => {
14 ws.current.send(JSON.stringify({ type: 'JOIN', room_id }));
15 };
16 ws.current.onmessage = (e) => {
17 const msg = JSON.parse(e.data);
18 if (msg.type === 'CHAT' || msg.type === 'NOTIF') {
19 setMessages((prev) => [...prev, msg]);
20 }
21 };
22 return () => ws.current.close();
23 }, [room_id]);
24
25 const sendMessage = () => {
26 ws.current.send(JSON.stringify({
27 type: 'CHAT',
28 sender: user,
29 message: input
30 }));
31 setInput('');
32 };
33
34 return (
35 <div>
36 <ul>
37 {messages.map((msg, i) =>
38 <li key={i}><b>{msg.sender || 'System'}</b>: {msg.message || msg.text}</li>
39 )}
40 </ul>
41 <input value={input} onChange={e => setInput(e.target.value)} />
42 <button onClick={sendMessage}>Send</button>
43 </div>
44 );
45}Usage Simulation
- Run the server.
- Open two browser tabs and join the same
room_id, for example “room1”. - Type and send a message; it will appear in both tabs in real time.
6. Handling Scale: Broadcast and Distribution
The initial system above is sufficient for tens to hundreds of active users. However, at the scale of thousands of connections, memory and the socket broadcast process must be distributed.
The principle is:
- If there is more than one WebSocket server, relay messages using a message broker (for example, Redis Pub/Sub).
- Message delivery between server nodes is done synchronously, so that users in a room spread across different servers still receive messages in real time.
Flow diagram with Redis as pub/sub:
sequenceDiagram
participant UserA as Client A
participant Node1 as WebSocket Node 1
participant Node2 as WebSocket Node 2
participant Redis as Redis Pub/Sub
UserA->>Node1: Send message (room1)
Node1->>Redis: Publish (room1, message)
Redis->>Node2: Subscribe (room1, message)
Node2->>UserB: Forward message
With an architecture like this, each server only needs to broadcast to the clients connected to it, while the data across nodes stays in sync.
7. Pros & Cons Table for the Simple WebSocket Solution
| Aspect | Strengths | Weaknesses |
|---|---|---|
| Real-time | Very low latency | Potential bottleneck on a single node |
| Simplicity | Easy to implement, fast development | Needs a redesign for high scale |
| Scalability | Easy to scale horizontally with pub/sub | Additional message broker infra |
| Store Chat History | Free to integrate a NoSQL DB | Needs limits on DB growth |
| Security | Easy to add authentication/authorization via the ws handshake | Needs a secure channel (wss, token, etc.) |
8. Conclusion
Building a real-time chat service requires an understanding of networking components, databases, and system scalability. The case study above provides a basic picture of implementing WebSocket-based chat, from the backend through to the client. Of course, in an industry or production-scale setting, more attention is needed on aspects such as authentication, rate limiting, message retention, logging, monitoring, and more. Even so, the foundation built from this case study is solid enough to grow into a scalable chat product!
What about the real-time chat architectures you’ve built? Don’t hesitate to share and discuss them in the comments!
Happy coding, and keep sharing knowledge!