Skip to content
All projects

IRC-HAT / Distributed systems

Distributed Chat System

I built a distributed chat system in Java with custom TCP protocols, gossip-based message replication, and a replicated addressing service for discovery and coordination.

System architecture

Clients connect to a chat server and exchange messages through a network of peers. A primary addressing server and its replicas track membership and available capacity. Discovery and registry updates use a separate control protocol from chat traffic.

Distributed Chat System

Explore the architecture
Peers per serverTwo available in this example
Messages: Distributed Chat SystemClient 1 sends to chat server A, which logs the message and echoes it in local broadcast. A selects the next n available peers in cyclic PID order, excluding itself. With fan-out two, A sends directly to B and C. Receiving servers repeat the selection rule. Selected onward paths illustrate propagation and duplicate suppression; C delivers to its local client. Illustrated fan-out: 2; server A forwards directly to B and C.Discovery & coordinationChat serversSeen ID → stop repeatSend / echoDeliverPrimaryaddressing serverAddressingreplicasChat serverAChat serverBChat serverCClient 1Client 2Messages: Distributed Chat SystemClient 1 sends to chat server A, which logs the message and echoes it in local broadcast. A selects the next n available peers in cyclic PID order, excluding itself. With fan-out two, A sends directly to B and C. Receiving servers repeat the selection rule. Selected onward paths illustrate propagation and duplicate suppression; C delivers to its local client. Illustrated fan-out: 2; server A forwards directly to B and C.Discovery & coordinationChat serversSeen ID → stop repeatSend / echoDeliverPrimaryaddressingserverAddressingreplicasChatserver AChatserver BChatserver CClient 1Client 2

Broadcast locally, then propagate

The server logs a new message, broadcasts it to local clients including its sender, and forwards it to a configurable number of peers. Seen IDs identify repeat copies.

  1. Client sends
  2. Local log + broadcast / sender echo
  3. Selected peers repeat propagation

Each server selects up to n connected peers after its own PID in sorted order, wrapping at the end. Receiving servers repeat the same rule; seen message IDs suppress repeat processing. Increasing fan-out adds propagation paths and network traffic.

Fan-out is configurable up to all available peers. The highlighted paths show server A’s targets and selected onward hops, not every transmission. The sender’s echo confirms local server receipt.

Custom TCP networking

I implemented the application protocols and connection lifecycle across the client, chat server, and addressing server, with shared message types in a common Gradle module. Typed JSON envelopes distinguish chat messages from registry updates, acknowledgments, and election messages.

Java NIO selectors multiplex server connections. Per-connection buffers assemble newline-delimited messages from TCP reads; chat output queues retain partially written buffers until the socket is ready again. The chat server dispatches framed messages to chat, peer, or addressing handlers through a fixed pool of eight workers.

The terminal client runs input, sending, receiving, and rendering on separate workers. Queues coordinate outgoing messages and pending-send state while JLine handles terminal input.

Discovery and registry replication

The addressing primary tracks server addresses, status, and client counts. Discovery selects the first active server with advertised capacity and returns its address. The client then opens a persistent TCP connection to that chat server and registers its username. New chat servers register separately, receive a network process ID and peer records, and connect to discovered peers.

I implemented passive registry replication with correlated update and acknowledgment messages. For tracked registrations, a pending event records the replicas still expected to reply and defers the registration response. A watchdog resends the update to those recipients; retry exhaustion removes unresponsive recipients so registration can proceed. Client-count updates and periodic record refreshes use separate, untracked update paths.

This concentrates membership changes in the primary and adds synchronization work during registration. Live chat propagation runs independently between peers, so each message avoids that registry coordination path.

Message propagation and history

A chat server logs an incoming message, broadcasts it to its local clients, and forwards it to selected peers. The sender receives that same message as an echo and clears its pending-send state. Receiving peers repeat the log, broadcast, and forwarding path, checking a seen-message-ID set for repeats.

Gossip has a configurable fan-out: each server selects up to n connected peers, excluding itself. Selection starts after its own process ID in sorted order and wraps around as needed. Each receiving server applies the same rule. A larger fan-out creates more propagation paths at the cost of additional traffic and duplicate copies; smaller values spread messages through more hops. Live propagation runs asynchronously.

I built append-only JSON-line storage with UUID-keyed metadata, byte offsets, and a timestamp index. History retrieval selects recent IDs from that index, then reads their recorded offsets. Storage and log-merge handlers suppress repeated serialized messages using SHA-256 hashes. Rewriting the JSON metadata index on each append keeps the file format simple but adds write work as history grows.

Failure detection and recovery

Chat peers probe idle connections with PING/PONG and remove peers that remain unresponsive. On a closed chat connection, the client queries addressing again and re-registers with the returned host. Initial registration also requests recent history. Outgoing queues retain unsent work, and thrown send errors requeue the affected message.

Joining and returning servers request peer logs to recover message history. I implemented the full-log request/response messages and a merge handler that appends records through the storage duplicate check. Addressing replicas synchronize separately by requesting server records from the primary.

Bully election runs among addressing processes after a failed primary is removed. A candidate contacts higher PIDs; a higher process replies and starts its own election. The lower candidate waits for a leader announcement and restarts if none arrives. A candidate with no higher known PID, or no higher reply before timeout, announces itself as primary.

The new primary’s connection details are distributed so clients and chat servers can discover it and reconnect. Addressing replicas re-register with the new leader and reload membership records, restoring registry coordination under the elected primary.

Tools & Technologies

  • Java
  • Java NIO
  • TCP
  • JSON
  • Jackson
  • org.json
  • JLine
  • Gradle
  • Docker
  • Docker Compose

I built connection state, message framing, dispatch, and queued chat writes around Java NIO channels and selectors. Jackson serializes typed wire messages; org.json supports the storage index and peer-record adaptation. JLine provides terminal editing alongside the client’s custom input, send, receive, and rendering workers.

Gradle organizes the client, chat server, addressing server, and shared protocol modules. Multistage Docker builds compile the application JARs inside a Java 21 build environment and copy them into Java 21 runtime images. Building inside containers kept JDK and dependency setup consistent across developer machines.

Docker Compose profiles run the process types individually or together on a shared bridge network. Environment variables supply advertised addresses and listener ports, while host port mappings let multiple services run locally without competing for the same port. This made the multi-process setup repeatable during development.