VOOZH about

URL: https://dev.to/bejoy_jbt_c466b154c469362/building-a-real-time-chat-platform-in-java-from-scratch-5923

⇱ Building a Real-Time Chat Platform in Java from Scratch - DEV Community


Building a Real-Time Chat Platform in Java from Scratch

Most chat tutorials start with a framework that hides the interesting parts. I wanted to see what it takes to build a real-time messaging system with plain Java — so I built BroadcastHub, a terminal-based chat platform powered by WebSockets.

This article walks through the problem, architecture, protocol design, persistence, private channels, Docker deployment, and what I learned along the way.

GitHub: https://github.com/bejoy-jbt/BroadcastHub

Docker: docker pull bejoy1514/broadcasthub:latest


The Problem

I set out to build something that could:

  1. Connect multiple terminal clients to one server in real time
  2. Support channels (like Slack rooms, but in the terminal)
  3. Allow private channels with invite codes
  4. Support direct messages between users
  5. Persist channels, message history, and bans across restarts
  6. Provide basic moderation tools for an admin

The constraint: no Spring Boot, no database — keep the stack small enough to understand every layer.


Architecture

BroadcastHub follows a layered design around a single WebSocket server:

Terminal Client
 │
 ▼
WebSocket Server (BroadcastServer)
 │
 ┌────────────────┐
 │ ClientRegistry │ Maps WebSocket ↔ username
 └────────────────┘
 │
 ┌────────────────┐
 │ ChannelManager │ Channels, membership, invite codes
 └────────────────┘
 │
 ┌────────────────┐
 │ MessageHistory │ Per-channel message deque (max 100)
 └────────────────┘
 │
 ┌────────────────┐
 │ AdminManager │ Mutes, bans, moderation state
 └────────────────┘
 │
 ┌────────────────┐
 │ Persistence │ Jackson → data/*.json
 └────────────────┘

Key classes:

Component Responsibility
BroadcastServer WebSocket lifecycle, registration, routing
ClientRegistry Username registration, online user lookup
CommandProcessor Slash commands (/join, /create, /msg)
ChannelManager Channel CRUD, membership, invite validation
MessageHistory In-memory history with disk backup
*Store classes JSON read/write via Jackson

Entry point:

java -jar broadcasthub.jar server # start server
java -jar broadcasthub.jar client # start client

WebSocket Design

I deliberately chose a plain-text protocol instead of JSON messages. In a terminal client, you can read the wire format directly.

Registration

First message from client must be:

REGISTER|bejoy

Server responds:

REGISTER_SUCCESS|bejoy

or REGISTER_FAILED|reason.

Chat messages

After registration, plain text is broadcast to the user's current channel:

Hello everyone!

Server formats and relays:

[bejoy] Hello everyone!

Slash commands

Messages starting with / are intercepted by CommandProcessor:

/join gaming
/create java private
/msg alex Hey!
/help

Why this works

  • Easy to test with wscat or any WebSocket client
  • No serialization overhead for a learning project
  • Clear separation: registration → commands → chat

The server uses Java-WebSocket (org.java-websocket) for both server and client.


Persistence

BroadcastHub stores data under data/:

data/
├── channels.json # channel metadata + invite codes
├── history.json # per-channel message history
└── bans.json # banned usernames

Jackson handles serialization. On startup, stores load into memory; on change, they write back to disk.

Design choices:

  • History capped at 100 messages per channel — prevents unbounded memory growth
  • Bans persisted, mutes in-memory — bans are security-sensitive; mutes are session-level
  • Channels saved on create/delete — invite codes survive restarts

Example channels.json:

{"general":{"name":"general","privateChannel":false,"inviteCode":null},"java":{"name":"java","privateChannel":true,"inviteCode":"A7KD91"}}

Private Channels

Private channels add an invite code at creation:

/create java private

Server responds:

Private Channel Created
Code : A7KD91

To join:

/join java A7KD91

ChannelManager.canJoin() validates the code before switching the user's channel. On join, the server sends the last 10 messages so users have context.

Public channels skip invite validation — anyone can /join gaming.


Docker Deployment

A multi-stage Dockerfile builds with Maven and runs on JRE 17 Alpine:

# Build stage: mvn package
# Runtime stage: copy fat JAR, expose 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
CMD ["server"]

Run server:

docker run -p 8080:8080 -v broadcasthub-data:/app/data bejoy1514/broadcasthub

Run client:

docker run -it --rm bejoy1514/broadcasthub client host.docker.internal 8080

Mounting /app/data keeps channels, history, and bans across container restarts.


Lessons Learned

1. Concurrent collections are non-negotiable

Multiple WebSocket threads read and write shared maps. ConcurrentHashMap and ConcurrentLinkedDeque prevented race conditions without manual locking everywhere.

2. Docker breaks stdin-based admin consoles

An admin console reading System.in fails in non-interactive containers. I added client-side admin via @@stats, @@ban user, etc. — pragmatic for Docker users.

3. Keep command routing separate

CommandProcessor owns user slash commands. BroadcastServer owns admin commands. Mixing them would have made both harder to extend.

4. File persistence is enough (for now)

For a portfolio project, JSON files beat setting up PostgreSQL. The tradeoff: no queryable history, no horizontal scaling — acceptable for v1.

5. Ship a one-command install

Publishing to Docker Hub (bejoy1514/broadcasthub) got more people trying it than "clone, mvn package, run" ever would.


What's Next

  • Web UI client
  • Secure WebSockets (WSS)
  • Admin authentication
  • Channel ownership and permissions

Try It

git clone https://github.com/bejoy-jbt/BroadcastHub.git
cd BroadcastHub
mvn clean package
java -jar target/broadcasthub-1.0-SNAPSHOT.jar server

Or pull the Docker image:

docker pull bejoy1514/broadcasthub:latest

Star the repo if you find it useful: https://github.com/bejoy-jbt/BroadcastHub


Questions or feedback? Leave a comment — I'd love to hear how you'd extend this.