Building a real-time chat application is an excellent way to explore modern reactive programming techniques in Java. By leveraging Spring Boot, WebFlux, and MongoDB, we can create a fully reactive WebSocket-based chat service that handles high concurrency and provides seamless, real-time communication.

This article walks you through the architecture, setup, and implementation details of a Reactive Spring Boot WebSocket Chat backed by MongoDB. We will focus on scalability and responsiveness using Project Reactor’s reactive streams.

Understanding the Technology Stack

Before diving into coding, let’s break down the key technologies:

  • Spring Boot: Simplifies application configuration and deployment.

  • Spring WebFlux: A reactive alternative to Spring MVC built on Project Reactor, supporting non-blocking and asynchronous request handling.

  • WebSocket: A protocol enabling full-duplex communication between client and server. Perfect for real-time messaging.

  • MongoDB: A NoSQL document database that integrates seamlessly with reactive programming through the Reactive MongoDB Driver.

The combination of these technologies allows us to handle a large number of concurrent connections efficiently without blocking threads.

Project Structure and Dependencies

Let’s start by creating a Spring Boot project. You can use Spring Initializr or manually set up the project.

Add the following essential dependencies in your build.gradle (or pom.xml if using Maven):

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.projectreactor:reactor-core'
implementation 'com.fasterxml.jackson.core:jackson-databind'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

These dependencies include Spring WebFlux, Reactive MongoDB, and WebSocket support.

Your project structure might look like this:

src/main/java/com/example/chat
├── config
│ └── WebSocketConfig.java
├── controller
│ └── ChatHandler.java
├── model
│ └── ChatMessage.java
├── repository
│ └── ChatRepository.java
└── ChatApplication.java

Defining the Domain Model

We need a simple domain model to represent a chat message.

package com.example.chat.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.Instant;

@Document(collection = “messages”)
public class ChatMessage {

@Id
private String id;
private String sender;
private String content;
private Instant timestamp;

public ChatMessage() {}

public ChatMessage(String sender, String content, Instant timestamp) {
this.sender = sender;
this.content = content;
this.timestamp = timestamp;
}

// Getters and setters
}

The ChatMessage class maps directly to our MongoDB collection named messages. The timestamp field ensures we can order messages chronologically.

Reactive MongoDB Repository

Next, create a repository interface using ReactiveMongoRepository to interact with the database in a fully non-blocking way:

package com.example.chat.repository;

import com.example.chat.model.ChatMessage;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import reactor.core.publisher.Flux;

public interface ChatRepository extends ReactiveMongoRepository<ChatMessage, String> {
Flux<ChatMessage> findBySender(String sender);
}

This allows us to fetch or save messages reactively. We can also define custom queries, such as retrieving messages by sender.

Configuring the WebSocket

We need to configure a WebSocket endpoint to enable real-time communication.

package com.example.chat.config;

import com.example.chat.controller.ChatHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;

import java.util.Map;

@Configuration
public class WebSocketConfig {

@Bean
public SimpleUrlHandlerMapping handlerMapping(ChatHandler chatHandler) {
return new SimpleUrlHandlerMapping(Map.of(“/ws/chat”, chatHandler), 10);
}

@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}

Here, we map the WebSocket endpoint /ws/chat to a handler class ChatHandler that will process incoming and outgoing messages.

Creating the WebSocket Handler

The ChatHandler will handle all WebSocket events such as receiving messages, saving them to MongoDB, and broadcasting to all connected clients.

package com.example.chat.controller;

import com.example.chat.model.ChatMessage;
import com.example.chat.repository.ChatRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
import org.springframework.web.reactive.socket.WebSocketMessage;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Instant;

@Component
public class ChatHandler implements WebSocketHandler {

private final ChatRepository chatRepository;
private final ObjectMapper objectMapper = new ObjectMapper();

public ChatHandler(ChatRepository chatRepository) {
this.chatRepository = chatRepository;
}

@Override
public Mono<Void> handle(WebSocketSession session) {
Flux<WebSocketMessage> output = session.receive()
.map(WebSocketMessage::getPayloadAsText)
.flatMap(this::saveAndBroadcast)
.map(session::textMessage);

return session.send(output);
}

private Flux<String> saveAndBroadcast(String jsonMessage) {
try {
ChatMessage chatMessage = objectMapper.readValue(jsonMessage, ChatMessage.class);
chatMessage.setTimestamp(Instant.now());
return chatRepository.save(chatMessage)
.thenMany(chatRepository.findAll()
.map(this::toJson));
} catch (Exception e) {
return Flux.error(e);
}
}

private String toJson(ChatMessage message) {
try {
return objectMapper.writeValueAsString(message);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

How It Works:

  • We listen for incoming WebSocket messages (session.receive()).

  • Each received message is parsed, stored in MongoDB, and then we retrieve the full message list to broadcast updates back to all clients.

  • We return a reactive Flux<WebSocketMessage> that keeps streaming as new messages arrive.

Application Main Class

The main application class boots everything up:

package com.example.chat;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ChatApplication {
public static void main(String[] args) {
SpringApplication.run(ChatApplication.class, args);
}
}

Run this class, and the server will start on the default port 8080.

Front-End WebSocket Client (Example)

Although this article focuses on the backend, here’s a simple HTML/JavaScript client to test the WebSocket chat:

<!DOCTYPE html>
<html>
<head>
<title>Reactive Chat</title>
</head>
<body>
<h1>Reactive Chat</h1>
<input id="sender" placeholder="Your name">
<input id="message" placeholder="Type a message">
<button onclick="sendMessage()">Send</button>
<ul id="chat"></ul>
<script>

const ws = new WebSocket(“ws://localhost:8080/ws/chat”);
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
const li = document.createElement(“li”);
li.textContent = `${msg.sender}: ${msg.content}`;
document.getElementById(“chat”).appendChild(li);
};function sendMessage() {
const sender = document.getElementById(“sender”).value;
const content = document.getElementById(“message”).value;
ws.send(JSON.stringify({ sender, content }));
}
</script>
</body>
</html>

This minimal client connects to the backend WebSocket endpoint and updates the chat list whenever a new message is received.

Testing and Running

  1. Make sure MongoDB is running locally or in a container (mongod on default port 27017).

  2. Start the Spring Boot application.

  3. Open multiple browser tabs with the HTML client.

  4. Send messages simultaneously and observe the real-time updates across all tabs.

Because we use WebFlux and Reactive MongoDB, the application can handle a high number of concurrent users efficiently.

Enhancements and Best Practices

  • Authentication: Secure the chat with Spring Security and JWT tokens.

  • Message Rooms: Extend the model to include chat rooms or private messaging.

  • Error Handling: Implement robust error handling for invalid JSON payloads.

  • Deployment: Deploy to a cloud service like AWS or Azure, using a managed MongoDB instance.

Conclusion

Building a Reactive Spring Boot WebSocket Chat with WebFlux and MongoDB demonstrates the power of reactive programming in handling real-time communication at scale. We configured a non-blocking WebSocket server, persisted messages with Reactive MongoDB, and streamed live updates to connected clients.

This architecture offers significant advantages:

  • Scalability: Non-blocking I/O allows thousands of concurrent connections.

  • Performance: Reactive streams reduce resource usage and optimize data flow.

  • Flexibility: MongoDB’s schema-less design makes it easy to adapt to new requirements.

By combining these technologies, you can create production-grade, real-time applications that remain highly responsive under heavy load. Whether you are building a simple chatroom, a collaborative tool, or any event-driven system, the techniques covered here provide a solid foundation for reactive, cloud-ready solutions.