This article targets Spring Boot 3.x (Java 17 or later).
Have you ever felt comfortable building REST HTTP APIs, but unsure how to implement real-time features like chat or notifications? That’s exactly where WebSocket comes in.
Spring Boot lets you implement real-time communication with minimal configuration by combining STOMP and SockJS. This article walks you through building a broadcast chat feature from scratch, step by step.
Understanding the Relationship Between WebSocket, STOMP, and SockJS
Let’s start by clarifying the role of each technology.
- WebSocket is a transport-layer protocol that enables bidirectional communication. Unlike HTTP’s request-response model, both the server and client can send messages to each other at any time.
- STOMP (Simple Text Oriented Messaging Protocol) is a messaging protocol that runs on top of WebSocket and provides a publish/subscribe model. It lets you structure communication as “subscribe to this topic” and “send a message to this topic.”
- SockJS is a fallback library for browsers and network environments that don’t support WebSocket. It automatically switches to an HTTP-based alternative when WebSocket is unavailable.
Spring Boot supports this entire stack with a single dependency: spring-boot-starter-websocket.
Adding the Dependency
For Maven, add the following to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
For Gradle:
implementation 'org.springframework.boot:spring-boot-starter-websocket'
Implementing the WebSocket Configuration Class
This is the core configuration class. Enable @EnableWebSocketMessageBroker and configure the endpoints and broker.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// The endpoint clients connect to. withSockJS() enables the fallback.
registry.addEndpoint("/ws").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// Messages prefixed with /app are routed to @MessageMapping handlers in the application
registry.setApplicationDestinationPrefixes("/app");
// Messages prefixed with /topic are delivered to all subscribers by the in-memory broker
registry.enableSimpleBroker("/topic");
}
}
/app is the prefix for messages sent from the client to the server, while /topic is the prefix for broadcast destinations. Understanding the distinction between these two roles will make the rest of the flow much easier to follow.
Implementing the Message Handler
Create a controller that receives messages from clients. Define the DTOs alongside it.
public class ChatMessage {
private String sender;
private String content;
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
@Controller
public class ChatController {
@MessageMapping("/chat") // Receives messages sent to /app/chat
@SendTo("/topic/messages") // Broadcasts the reply to all subscribers of /topic/messages
public ChatMessage handleMessage(ChatMessage message) {
return message;
}
}
@MessageMapping works just like @RequestMapping for HTTP. Spring automatically interprets the /app prefix, so you only need to write /chat here.
Broadcasting from the Server Side (SimpMessagingTemplate)
When you want to proactively push messages from outside a controller — such as for scheduled notifications or event-triggered pushes — use SimpMessagingTemplate.
@Service
public class NotificationService {
private final SimpMessagingTemplate messagingTemplate;
public NotificationService(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
public void sendNotification(String message) {
messagingTemplate.convertAndSend("/topic/notifications", message);
}
}
The first argument to convertAndSend is the topic path, and the second is the payload. This can be called from @Scheduled tasks or other service classes.
Implementing the JavaScript Client
On the frontend, use SockJS and @stomp/stompjs (v5 or later). The following HTML can be opened directly in a browser to verify functionality.
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Chat</title>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs/bundles/stomp.umd.min.js"></script>
</head>
<body>
<input id="message" placeholder="Enter a message">
<button onclick="sendMessage()">Send</button>
<div id="output"></div>
<script>
const client = new StompJs.Client({
webSocketFactory: () => new SockJS('/ws')
});
client.onConnect = () => {
client.subscribe('/topic/messages', (msg) => {
const body = JSON.parse(msg.body);
document.getElementById('output').innerHTML +=
`<p>${body.sender}: ${body.content}</p>`;
});
};
client.activate();
function sendMessage() {
const content = document.getElementById('message').value;
client.publish({
destination: '/app/chat',
body: JSON.stringify({ sender: 'user1', content: content })
});
}
</script>
</body>
</html>
The older Stomp.over(socket) pattern belongs to the stompjs (v2.x) API. It has been unmaintained since 2015 and is deprecated — @stomp/stompjs is now the recommended successor.
Integrating Spring Security with WebSocket
In projects that use Spring Security, WebSocket endpoints may be blocked with a 403 error. The following configuration resolves this:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/ws/**").permitAll() // Allow the WebSocket endpoint
.anyRequest().authenticated()
)
.csrf(csrf -> csrf
.ignoringRequestMatchers("/ws/**") // Exclude WebSocket from CSRF protection
);
return http.build();
}
}
WebSocket is inherently incompatible with HTTP’s CSRF token mechanism, so it is standard practice to disable CSRF protection specifically for WebSocket endpoints. If you want to restrict access to authenticated users only, replace permitAll() with authenticated().
Note that this configuration applies to HTTP handshake-level protection. Access control at the WebSocket message level — such as which topics a given user can subscribe to or publish on — is outside this scope. For details on combining this with JWT-based authentication, see How to Implement JWT Authentication in Spring Boot.
Verifying the Behavior
Once the application is running, open the HTML file in multiple browser tabs. Send a message from one tab and confirm it appears in the other — this verifies that broadcasting is working correctly.
In Chrome DevTools, open the Network tab and filter by WS to inspect the STOMP frames being sent and received. You should see a CONNECT frame flowing through when the connection is established.
If things aren’t working, check the following:
- 403 errors are almost always caused by Spring Security blocking the WebSocket endpoint. Review your
permitAll()configuration. - Connection succeeds but messages don’t arrive is usually a misconfiguration of the
/appand/topicprefixes. Double-check the paths used for publishing and subscribing. - CORS errors can be resolved by adding
.setAllowedOrigins("*")toregisterStompEndpoints, but limit this to development and testing only. In production, specify allowed origins explicitly or consider usingsetAllowedOriginPatterns. See the Spring Boot CORS Configuration Guide for details.
Summary
We’ve walked through implementing real-time communication with Spring Boot + STOMP + SockJS. The fundamental structure is three layers: define endpoints and the broker in the configuration class, receive messages with @MessageMapping, and send push notifications with SimpMessagingTemplate.
If you need to use interceptors for WebSocket request processing, check out The Difference Between Filter and Interceptor and How to Use Them as well.