Skip to content

A real-time chat application enabling instant messaging using WebSockets for seamless client-server communication. Supports multi-user interactions with fast, event-driven message delivery. Built to demonstrate scalable backend integration and responsive UI design.

License

Notifications You must be signed in to change notification settings

shivrajcodez/ChatWave

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

⬑ ChatWave

A real-time chat application built with Spring Boot and WebSockets

Java Spring Boot WebSocket License: MIT

Features Β· Architecture Β· Quick Start Β· API Reference Β· Production


Overview

ChatWave is a full-stack, real-time messaging app that demonstrates how to build a production-quality WebSocket system with Spring Boot. It supports multiple chat rooms, live online user tracking, persistent message history, and real-time typing indicators β€” all running on a single server with zero external dependencies (H2 in-memory DB, Spring's built-in STOMP broker).

The project is intentionally structured to showcase three core backend concepts: WebSocket communication, asynchronous message handling, and real-time system design.


✨ Features

  • Multiple chat rooms β€” Pre-seeded channels (#general, #tech, #random, #announcements) with the ability to create new ones via REST
  • Online user tracking β€” Per-room and global user counts updated in real time via WebSocket connect/disconnect events
  • Message persistence β€” All messages stored to a JPA database, retrieved asynchronously on room join (last 50 messages)
  • Typing indicator β€” Debounced, pub/sub typing events broadcast per room, auto-cleared after 2.5 seconds of inactivity
  • Message history β€” Delivered privately to each user on join via a user-specific STOMP queue (not broadcast to the whole room)
  • Auto-reconnect β€” SockJS fallback transport with client-side reconnect on disconnect
  • Async persistence β€” Messages are broadcast to clients immediately; DB writes happen on a dedicated thread pool in the background
  • REST API β€” HTTP endpoints for rooms, users, and message history

πŸ— Architecture

System Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Client                           β”‚
β”‚              SockJS + STOMP.js + Vanilla JS             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ WebSocket / HTTP Fallback
                         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Spring Boot Server                   β”‚
β”‚                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  REST Layer  β”‚    β”‚      WebSocket Layer          β”‚  β”‚
β”‚  β”‚   /api/*     β”‚    β”‚   /ws  (SockJS endpoint)      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                     β”‚                   β”‚
β”‚                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚                      β”‚       ChatController          β”‚  β”‚
β”‚                      β”‚   @MessageMapping handlers    β”‚  β”‚
β”‚                      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                     β”‚                   β”‚
β”‚           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚           β–Ό                         β–Ό              β–Ό   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ MessageService β”‚  β”‚OnlineUserService β”‚  β”‚RoomServiceβ”‚β”‚
β”‚  β”‚ (@Async write) β”‚  β”‚ (ConcurrentMap)  β”‚  β”‚  (JPA)   β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚          β”‚                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚         In-Memory STOMP Message Broker          β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚               H2 Database (JPA)                 β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

STOMP Topic Map

Destination Direction Purpose
/app/chat.join Client β†’ Server Join a room
/app/chat.send Client β†’ Server Send a message
/app/chat.typing Client β†’ Server Typing status update
/app/chat.leave Client β†’ Server Leave a room
/topic/room/{id} Server β†’ Client Broadcast messages to a room
/topic/room/{id}/typing Server β†’ Client Typing events for a room
/topic/room/{id}/users Server β†’ Client Live user list for a room
/topic/online-count Server β†’ Client Global online count
/user/queue/history Server β†’ Client Private message history on join
/user/queue/errors Server β†’ Client Private error delivery

Message Flows

Joining a room:

Client                                    Server
  │──── STOMP CONNECT ─────────────────▢ β”‚
  │◀─── CONNECTED ──────────────────────  β”‚
  │──── SUBSCRIBE /topic/room/{id} ─────▢ β”‚
  │──── SUBSCRIBE /user/queue/history ──▢ β”‚
  │──── /app/chat.join ─────────────────▢ β”‚
  β”‚                                        │── register in OnlineUserService
  β”‚                                        │── persist JOIN msg (async)
  │◀─── /user/queue/history ────────────  β”‚  ← last 50 msgs, private to this user
  │◀─── /topic/room/{id} ───────────────  β”‚  ← JOIN system message (broadcast)
  │◀─── /topic/room/{id}/users ─────────  β”‚  ← updated user list (broadcast)

Sending a message:

Client                                    Server
  │──── /app/chat.send ─────────────────▢ β”‚
  β”‚                                        │── broadcast IMMEDIATELY
  │◀─── /topic/room/{id} ───────────────  β”‚  ← all subscribers receive it
  β”‚                                        │── persist to DB (async, non-blocking)

Typing indicator:

Client A                    Server                    Client B
  │── /app/chat.typing ──▢  β”‚                             β”‚
  β”‚    {typing: true}        │── /topic/room/{id}/typing ─▢ β”‚
  β”‚                          β”‚                             β”‚
  β”‚  [2.5s no input]         β”‚                             β”‚
  │── /app/chat.typing ──▢  β”‚                             β”‚
  β”‚    {typing: false}       │── /topic/room/{id}/typing ─▢ β”‚

πŸ“ Project Structure

src/main/java/com/chatapp/
β”œβ”€β”€ ChatApplication.java              # Entry point (@EnableAsync)
β”‚
β”œβ”€β”€ config/
β”‚   β”œβ”€β”€ WebSocketConfig.java          # STOMP broker + SockJS endpoint
β”‚   β”œβ”€β”€ AsyncConfig.java              # Thread pool for async DB writes
β”‚   β”œβ”€β”€ WebSocketEventListener.java   # Connect/disconnect event hooks
β”‚   └── DataInitializer.java          # Seeds rooms + welcome messages on startup
β”‚
β”œβ”€β”€ model/
β”‚   β”œβ”€β”€ Message.java                  # JPA entity (CHAT / JOIN / LEAVE / SYSTEM)
β”‚   β”œβ”€β”€ ChatRoom.java                 # JPA entity
β”‚   β”œβ”€β”€ UserSession.java              # In-memory only, not persisted
β”‚   └── ChatDTOs.java                 # All WebSocket payload DTOs (inbound + outbound)
β”‚
β”œβ”€β”€ repository/
β”‚   β”œβ”€β”€ MessageRepository.java        # findLastMessagesByRoomId w/ Pageable
β”‚   └── ChatRoomRepository.java
β”‚
β”œβ”€β”€ service/
β”‚   β”œβ”€β”€ MessageService.java           # @Async saveMessageAsync + history fetch
β”‚   β”œβ”€β”€ RoomService.java              # Room CRUD, default channel seeding
β”‚   └── OnlineUserService.java        # Thread-safe ConcurrentHashMap of live sessions
β”‚
└── controller/
    β”œβ”€β”€ ChatController.java           # All @MessageMapping WebSocket handlers
    β”œβ”€β”€ RoomController.java           # REST /api/* endpoints
    └── HomeController.java           # Serves the SPA shell

src/main/resources/
β”œβ”€β”€ application.properties
β”œβ”€β”€ templates/index.html              # Thymeleaf template (SPA shell)
└── static/
    β”œβ”€β”€ css/style.css                 # Dark terminal-inspired UI
    └── js/chat.js                    # STOMP client, typing debounce, DOM rendering

πŸš€ Quick Start

Prerequisites

Run locally

# Clone the repo
git clone https://github.com/your-username/chatwave.git
cd chatwave

# Run
mvn spring-boot:run

Open http://localhost:8080, pick a username, and start chatting. Open a second browser tab to simulate a second user.

Inspect the database

The H2 console is available at http://localhost:8080/h2-console while the app is running.

JDBC URL:  jdbc:h2:mem:chatdb
Username:  sa
Password:  (leave blank)

Build a JAR

mvn clean package -DskipTests
java -jar target/realtime-chat-1.0.0.jar

πŸ“‘ API Reference

Rooms

Method Endpoint Description
GET /api/rooms List all rooms with live online counts
POST /api/rooms Create a new room
GET /api/rooms/{id}/messages Last 50 messages in a room
GET /api/rooms/{id}/users Online users currently in a room
GET /api/stats Global stats (total online, room count)

Create a room:

curl -X POST http://localhost:8080/api/rooms \
  -H "Content-Type: application/json" \
  -d '{"name": "#design", "description": "UI/UX and design discussion"}'

Get recent messages:

curl http://localhost:8080/api/rooms/general/messages

βš™οΈ Configuration

Key settings in src/main/resources/application.properties:

server.port=8080

# H2 in-memory (swap for PostgreSQL in production)
spring.datasource.url=jdbc:h2:mem:chatdb
spring.h2.console.enabled=true

# JPA
spring.jpa.hibernate.ddl-auto=create-drop

Switching to PostgreSQL

Replace the H2 dependency in pom.xml:

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

Update application.properties:

spring.datasource.url=jdbc:postgresql://localhost:5432/chatwave
spring.datasource.username=your_user
spring.datasource.password=your_password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

🏭 Going to Production

The app runs fine out of the box for demos and single-node deployments. For production, work through this checklist:

Infrastructure

  • Replace H2 with PostgreSQL (see above)
  • Replace SimpleBroker with RabbitMQ or ActiveMQ for multi-node support β€” Spring's STOMP broker relay makes this a config-only change
  • Put the app behind nginx with WebSocket proxying and TLS (WSS)

Security

  • Add Spring Security β€” protect HTTP endpoints and validate auth tokens on WebSocket connect
  • Sanitize message content server-side (basic XSS escaping is already in ChatController.sanitize())
  • Rate-limit /app/chat.send per user

Reliability

  • Add cursor-based message pagination (the Pageable pattern in MessageRepository is already set up for this)
  • Store UserSession in Redis for recovery across server restarts
  • Add health and metrics endpoints via Spring Actuator

Multi-node STOMP relay (RabbitMQ) β€” swap this into WebSocketConfig.java:

// Replace enableSimpleBroker() with:
config.enableStompBrokerRelay("/topic", "/queue")
      .setRelayHost("localhost")
      .setRelayPort(61613)
      .setClientLogin("guest")
      .setClientPasscode("guest");

πŸ”§ Key Design Decisions

Why async persistence? WebSocket handlers run on a shared thread pool. Blocking on a DB write for every message degrades throughput under load. MessageService.saveMessageAsync() uses @Async with a dedicated ThreadPoolTaskExecutor β€” messages are broadcast to clients first, and the DB write follows in the background via CompletableFuture.

Why ConcurrentHashMap for online users? WebSocket connect/disconnect events fire from multiple threads. ConcurrentHashMap gives lock-free reads and fine-grained locking on writes β€” a good fit for a structure that's read constantly but written infrequently.

Why user-private queues for history? Message history is sent to /user/queue/history β€” a STOMP destination scoped to a single session. Publishing history to the room topic would re-broadcast it to all active users in the room, which would be wrong. Spring's convertAndSendToUser() handles the session scoping transparently.

Why SockJS? SockJS provides automatic fallback to HTTP long-polling when WebSockets are blocked (corporate proxies, firewalls). It's transparent to the STOMP layer and adds resilience for no cost.


πŸ“„ License

MIT β€” use it however you like.


Built with Spring Boot Β· WebSocket Β· STOMP Β· SockJS Β· H2

About

A real-time chat application enabling instant messaging using WebSockets for seamless client-server communication. Supports multi-user interactions with fast, event-driven message delivery. Built to demonstrate scalable backend integration and responsive UI design.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors