<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bejoy JBT</title>
    <description>The latest articles on DEV Community by Bejoy JBT (@bejoy_jbt_c466b154c469362).</description>
    <link>https://dev.to/bejoy_jbt_c466b154c469362</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2908264%2F406a7f18-3f8a-4fd9-b2e6-a0219fbd23ab.jpg</url>
      <title>DEV Community: Bejoy JBT</title>
      <link>https://dev.to/bejoy_jbt_c466b154c469362</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bejoy_jbt_c466b154c469362"/>
    <language>en</language>
    <item>
      <title>Building a Real-Time Chat Platform in Java from Scratch</title>
      <dc:creator>Bejoy JBT</dc:creator>
      <pubDate>Thu, 11 Jun 2026 18:57:52 +0000</pubDate>
      <link>https://dev.to/bejoy_jbt_c466b154c469362/building-a-real-time-chat-platform-in-java-from-scratch-5923</link>
      <guid>https://dev.to/bejoy_jbt_c466b154c469362/building-a-real-time-chat-platform-in-java-from-scratch-5923</guid>
      <description>&lt;h1&gt;
  
  
  Building a Real-Time Chat Platform in Java from Scratch
&lt;/h1&gt;

&lt;p&gt;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 &lt;strong&gt;BroadcastHub&lt;/strong&gt;, a terminal-based chat platform powered by WebSockets.&lt;/p&gt;

&lt;p&gt;This article walks through the problem, architecture, protocol design, persistence, private channels, Docker deployment, and what I learned along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/bejoy-jbt/BroadcastHub" rel="noopener noreferrer"&gt;https://github.com/bejoy-jbt/BroadcastHub&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Docker:&lt;/strong&gt; &lt;code&gt;docker pull bejoy1514/broadcasthub:latest&lt;/code&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I set out to build something that could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect multiple terminal clients to one server in real time&lt;/li&gt;
&lt;li&gt;Support &lt;strong&gt;channels&lt;/strong&gt; (like Slack rooms, but in the terminal)&lt;/li&gt;
&lt;li&gt;Allow &lt;strong&gt;private channels&lt;/strong&gt; with invite codes&lt;/li&gt;
&lt;li&gt;Support &lt;strong&gt;direct messages&lt;/strong&gt; between users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist&lt;/strong&gt; channels, message history, and bans across restarts&lt;/li&gt;
&lt;li&gt;Provide basic &lt;strong&gt;moderation&lt;/strong&gt; tools for an admin&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The constraint: &lt;strong&gt;no Spring Boot, no database&lt;/strong&gt; — keep the stack small enough to understand every layer.&lt;/p&gt;


&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;BroadcastHub follows a layered design around a single WebSocket server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
 └────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key classes:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BroadcastServer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;WebSocket lifecycle, registration, routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ClientRegistry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Username registration, online user lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CommandProcessor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Slash commands (&lt;code&gt;/join&lt;/code&gt;, &lt;code&gt;/create&lt;/code&gt;, &lt;code&gt;/msg&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ChannelManager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Channel CRUD, membership, invite validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MessageHistory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;In-memory history with disk backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;*Store&lt;/code&gt; classes&lt;/td&gt;
&lt;td&gt;JSON read/write via Jackson&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Entry point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; broadcasthub.jar server   &lt;span class="c"&gt;# start server&lt;/span&gt;
java &lt;span class="nt"&gt;-jar&lt;/span&gt; broadcasthub.jar client   &lt;span class="c"&gt;# start client&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  WebSocket Design
&lt;/h2&gt;

&lt;p&gt;I deliberately chose a &lt;strong&gt;plain-text protocol&lt;/strong&gt; instead of JSON messages. In a terminal client, you can read the wire format directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registration
&lt;/h3&gt;

&lt;p&gt;First message from client must be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REGISTER|bejoy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server responds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REGISTER_SUCCESS|bejoy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or &lt;code&gt;REGISTER_FAILED|reason&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chat messages
&lt;/h3&gt;

&lt;p&gt;After registration, plain text is broadcast to the user's current channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello everyone!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server formats and relays:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[bejoy] Hello everyone!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Slash commands
&lt;/h3&gt;

&lt;p&gt;Messages starting with &lt;code&gt;/&lt;/code&gt; are intercepted by &lt;code&gt;CommandProcessor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/join gaming
/create java private
/msg alex Hey!
/help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Easy to test with &lt;code&gt;wscat&lt;/code&gt; or any WebSocket client&lt;/li&gt;
&lt;li&gt;No serialization overhead for a learning project&lt;/li&gt;
&lt;li&gt;Clear separation: registration → commands → chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server uses &lt;strong&gt;Java-WebSocket&lt;/strong&gt; (&lt;code&gt;org.java-websocket&lt;/code&gt;) for both server and client.&lt;/p&gt;




&lt;h2&gt;
  
  
  Persistence
&lt;/h2&gt;

&lt;p&gt;BroadcastHub stores data under &lt;code&gt;data/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data/
├── channels.json   # channel metadata + invite codes
├── history.json    # per-channel message history
└── bans.json       # banned usernames
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Jackson&lt;/strong&gt; handles serialization. On startup, stores load into memory; on change, they write back to disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design choices:&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Example &lt;code&gt;channels.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"general"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"general"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"privateChannel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"inviteCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"java"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"java"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"privateChannel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"inviteCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A7KD91"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Private Channels
&lt;/h2&gt;

&lt;p&gt;Private channels add an invite code at creation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/create java private
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server responds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Private Channel Created
Code : A7KD91
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To join:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/join java A7KD91
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Public channels skip invite validation — anyone can &lt;code&gt;/join gaming&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker Deployment
&lt;/h2&gt;

&lt;p&gt;A multi-stage Dockerfile builds with Maven and runs on JRE 17 Alpine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build stage: mvn package&lt;/span&gt;
&lt;span class="c"&gt;# Runtime stage: copy fat JAR, expose 8080&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["java", "-jar", "app.jar"]&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["server"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run server:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="nt"&gt;-v&lt;/span&gt; broadcasthub-data:/app/data bejoy1514/broadcasthub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run client:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--rm&lt;/span&gt; bejoy1514/broadcasthub client host.docker.internal 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mounting &lt;code&gt;/app/data&lt;/code&gt; keeps channels, history, and bans across container restarts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Concurrent collections are non-negotiable
&lt;/h3&gt;

&lt;p&gt;Multiple WebSocket threads read and write shared maps. &lt;code&gt;ConcurrentHashMap&lt;/code&gt; and &lt;code&gt;ConcurrentLinkedDeque&lt;/code&gt; prevented race conditions without manual locking everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Docker breaks stdin-based admin consoles
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  3. Keep command routing separate
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CommandProcessor&lt;/code&gt; owns user slash commands. &lt;code&gt;BroadcastServer&lt;/code&gt; owns admin commands. Mixing them would have made both harder to extend.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. File persistence is enough (for now)
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  5. Ship a one-command install
&lt;/h3&gt;

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




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Web UI client&lt;/li&gt;
&lt;li&gt;Secure WebSockets (WSS)&lt;/li&gt;
&lt;li&gt;Admin authentication&lt;/li&gt;
&lt;li&gt;Channel ownership and permissions&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/bejoy-jbt/BroadcastHub.git
&lt;span class="nb"&gt;cd &lt;/span&gt;BroadcastHub
mvn clean package
java &lt;span class="nt"&gt;-jar&lt;/span&gt; target/broadcasthub-1.0-SNAPSHOT.jar server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or pull the Docker image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull bejoy1514/broadcasthub:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Star the repo if you find it useful: &lt;a href="https://github.com/bejoy-jbt/BroadcastHub" rel="noopener noreferrer"&gt;https://github.com/bejoy-jbt/BroadcastHub&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Questions or feedback? Leave a comment — I'd love to hear how you'd extend this.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>websocket</category>
      <category>docker</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
