Complete Guide to SpringBoot WebSockets: Real-time Communication Patterns

15 minute read

Introduction to WebSockets

WebSockets provide full-duplex communication between client and server over a single, persistent connection. Unlike HTTP’s request-response model, WebSockets enable real-time, bidirectional data flow.

WebSocket vs HTTP vs Server-Sent Events

flowchart TB
    subgraph HTTPReqRes ["HTTP Request-Response"]
        A1[Client] -->|Request| B1[Server]
        B1 -->|Response| A1
        A1 -->|New Request| B1
        B1 -->|New Response| A1
    end
    
    subgraph SSE ["Server-Sent Events (SSE)"]
        A2[Client] -->|Initial Request| B2[Server]
        B2 -->|Event Stream| A2
        B2 -->|Event| A2
        B2 -->|Event| A2
    end
    
    subgraph WebSocket ["WebSocket"]
        A3[Client] <-->|Bidirectional| B3[Server]
        A3 <-->|Real-time| B3
        A3 <-->|Persistent Connection| B3
    end
Feature HTTP Server-Sent Events WebSocket
Direction Request-Response Server → Client Bidirectional
Connection Short-lived Persistent (one-way) Persistent (two-way)
Overhead High (headers) Medium Low
Real-time No Yes Yes
Complexity Low Medium High
Use Cases REST APIs Live feeds, notifications Chat, gaming, collaboration

When to Use WebSockets

✅ Perfect for:

  • Real-time chat applications
  • Live gaming and multiplayer experiences
  • Collaborative editing (Google Docs style)
  • Live trading platforms
  • Real-time dashboards and monitoring
  • Live sports scores and updates

❌ Avoid for:

  • Simple request-response APIs
  • File uploads/downloads
  • SEO-dependent content
  • One-way data flow (use SSE instead)

Spring WebSocket Architecture

Overall Architecture

flowchart TB
    subgraph ClientSide ["Client Side"]
        C1[Web Browser]
        C2[Mobile App]
        C3[JavaScript Client]
    end
    
    subgraph SpringBootApp ["Spring Boot Application"]
        WS[WebSocket Endpoint]
        STOMP[STOMP Protocol Layer]
        MB[Message Broker]
        MH[Message Handler]
        SB[Spring Bean Services]
    end
    
    subgraph MessageBrokerOptions ["Message Broker Options"]
        SM[Simple Message Broker]
        RM[RabbitMQ]
        AM[ActiveMQ]
        RE[Redis]
    end
    
    C1 <-->|WebSocket| WS
    C2 <-->|WebSocket| WS
    C3 <-->|WebSocket| WS
    
    WS <--> STOMP
    STOMP <--> MB
    MB <--> MH
    MH <--> SB
    
    MB <--> SM
    MB <--> RM
    MB <--> AM
    MB <--> RE

Spring WebSocket Approaches

Spring Boot provides two main approaches for WebSocket implementation:

1. Raw WebSocket (@EnableWebSocket)

Use Case: Low-level WebSocket handling with custom protocols

sequenceDiagram
    participant Client
    participant WebSocketHandler
    participant BusinessService
    
    Client->>WebSocketHandler: connect()
    WebSocketHandler->>Client: afterConnectionEstablished()
    
    Client->>WebSocketHandler: TextMessage
    WebSocketHandler->>BusinessService: processMessage()
    BusinessService-->>WebSocketHandler: processedData
    WebSocketHandler->>Client: TextMessage (response)
    
    Client->>WebSocketHandler: close()
    WebSocketHandler->>Client: afterConnectionClosed()

Configuration

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyWebSocketHandler(), "/websocket")
               .setAllowedOrigins("*")
               .withSockJS(); // Fallback support
    }
}

WebSocket Handler

@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
    
    private final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();
    
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
        log.info("Client connected: {}", session.getId());
    }
    
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.info("Received: {}", payload);
        
        // Echo to all connected clients
        broadcastMessage(new TextMessage("Echo: " + payload));
    }
    
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.remove(session);
        log.info("Client disconnected: {}", session.getId());
    }
    
    private void broadcastMessage(TextMessage message) {
        sessions.parallelStream().forEach(session -> {
            try {
                session.sendMessage(message);
            } catch (Exception e) {
                log.error("Error sending message", e);
            }
        });
    }
}

2. STOMP WebSocket (@EnableWebSocketMessageBroker)

Use Case: High-level messaging with publish-subscribe patterns

sequenceDiagram
    participant Client
    participant STOMPEndpoint
    participant MessageBroker
    participant Controller
    participant Service
    
    Client->>STOMPEndpoint: CONNECT
    STOMPEndpoint->>Client: CONNECTED
    
    Client->>STOMPEndpoint: SUBSCRIBE /topic/messages
    STOMPEndpoint->>MessageBroker: Register subscription
    
    Client->>STOMPEndpoint: SEND /app/chat
    STOMPEndpoint->>Controller: @MessageMapping("/chat")
    Controller->>Service: processMessage()
    Service-->>Controller: processedMessage
    Controller->>MessageBroker: convertAndSend("/topic/messages")
    MessageBroker->>Client: MESSAGE (to all subscribers)

STOMP Configuration

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketMessageConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
               .setAllowedOriginPatterns("*")
               .withSockJS();
    }
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // Enable simple in-memory broker
        registry.enableSimpleBroker("/topic", "/queue");
        
        // Set application destination prefix
        registry.setApplicationDestinationPrefixes("/app");
        
        // Optional: Set user destination prefix
        registry.setUserDestinationPrefix("/user");
    }
}

STOMP Controller

@Controller
public class ChatController {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(ChatMessage chatMessage) {
        return chatMessage;
    }
    
    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(ChatMessage chatMessage, 
                              SimpMessageHeaderAccessor headerAccessor) {
        // Add username to websocket session
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        return chatMessage;
    }
    
    // Send to specific user
    @MessageMapping("/chat.private")
    public void sendPrivateMessage(@Payload PrivateMessage message, 
                                  SimpMessageHeaderAccessor headerAccessor) {
        messagingTemplate.convertAndSendToUser(
            message.getRecipient(), 
            "/queue/private", 
            message
        );
    }
}

Message Broker Patterns

Simple Message Broker vs External Broker

flowchart TB
    subgraph SimpleMessageBroker ["Simple Message Broker (In-Memory)"]
        SM[Simple Broker]
        SM --> T1[/topic/messages]
        SM --> T2[/queue/private]
        SM --> T3[/user/specific]
    end
    
    subgraph ExternalMessageBroker ["External Message Broker"]
        EB[RabbitMQ/ActiveMQ]
        EB --> E1[Exchange/Topic]
        EB --> E2[Queue]
        EB --> E3[Routing Key]
    end
    
    subgraph ScalingConsiderations ["Scaling Considerations"]
        S1[Single Instance] --> SM
        S2[Multiple Instances] --> EB
        S3[High Availability] --> EB
        S4[Persistence] --> EB
    end

External Broker Configuration

RabbitMQ Integration

@Configuration
@EnableWebSocketMessageBroker
public class RabbitMQWebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic", "/queue")
               .setRelayHost("localhost")
               .setRelayPort(61613)
               .setClientLogin("guest")
               .setClientPasscode("guest")
               .setSystemLogin("guest")
               .setSystemPasscode("guest");
               
        registry.setApplicationDestinationPrefixes("/app");
    }
}

Redis Pub/Sub Integration

@Configuration
public class RedisWebSocketConfig {
    
    @Bean
    public RedisMessageListenerContainer redisContainer() {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(jedisConnectionFactory());
        container.addMessageListener(messageListener(), new PatternTopic("chat.*"));
        return container;
    }
    
    @Bean
    public MessageListener messageListener() {
        return (message, pattern) -> {
            // Broadcast to WebSocket clients
            messagingTemplate.convertAndSend("/topic/messages", 
                new String(message.getBody()));
        };
    }
}

Data Models and Message Types

Message Structure

// Base message
@Data
public abstract class BaseMessage {
    private String id;
    private LocalDateTime timestamp;
    private MessageType type;
    private String sessionId;
}

// Chat message
@Data
@EqualsAndHashCode(callSuper = true)
public class ChatMessage extends BaseMessage {
    private String sender;
    private String content;
    private String roomId;
}

// System message
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemMessage extends BaseMessage {
    private String event;
    private Map<String, Object> data;
}

// Private message
@Data
@EqualsAndHashCode(callSuper = true)
public class PrivateMessage extends BaseMessage {
    private String sender;
    private String recipient;
    private String content;
}

public enum MessageType {
    CHAT, JOIN, LEAVE, TYPING, SYSTEM, PRIVATE
}

Real-World Use Cases and Implementations

1. Chat Application Architecture

flowchart TB
    subgraph ChatApplication ["Chat Application"]
        UI[Chat UI]
        WS[WebSocket Connection]
        CR[Chat Room Service]
        US[User Service]
        MS[Message Service]
        DB[(Database)]
    end
    
    UI <--> WS
    WS <--> CR
    CR <--> US
    CR <--> MS
    MS <--> DB
    
    subgraph MessageFlow ["Message Flow"]
        M1[User Types Message] --> M2[Send via WebSocket]
        M2 --> M3[Server Processes]
        M3 --> M4[Store in DB]
        M4 --> M5[Broadcast to Room]
        M5 --> M6[All Clients Receive]
    end

Chat Implementation

@Controller
public class ChatRoomController {
    
    @Autowired
    private ChatService chatService;
    
    @MessageMapping("/chat.join/{roomId}")
    @SendTo("/topic/room/{roomId}")
    public SystemMessage joinRoom(@DestinationVariable String roomId, 
                                 ChatUser user,
                                 SimpMessageHeaderAccessor headerAccessor) {
        
        headerAccessor.getSessionAttributes().put("roomId", roomId);
        headerAccessor.getSessionAttributes().put("username", user.getUsername());
        
        chatService.addUserToRoom(roomId, user);
        
        return SystemMessage.builder()
            .type(MessageType.JOIN)
            .event("user-joined")
            .data(Map.of("username", user.getUsername(), "roomId", roomId))
            .timestamp(LocalDateTime.now())
            .build();
    }
    
    @MessageMapping("/chat.send/{roomId}")
    @SendTo("/topic/room/{roomId}")
    public ChatMessage sendMessage(@DestinationVariable String roomId, 
                                  ChatMessage message) {
        // Save message to database
        ChatMessage savedMessage = chatService.saveMessage(roomId, message);
        
        // Return message to broadcast
        return savedMessage;
    }
    
    @MessageMapping("/chat.typing/{roomId}")
    @SendTo("/topic/room/{roomId}/typing")
    public TypingIndicator handleTyping(@DestinationVariable String roomId,
                                       TypingIndicator indicator) {
        return indicator;
    }
}

2. Live Dashboard/Monitoring

sequenceDiagram
    participant Dashboard
    participant WebSocket
    participant MetricsService
    participant Database
    participant ExternalAPI
    
    Dashboard->>WebSocket: Subscribe to /topic/metrics
    
    loop Every 5 seconds
        MetricsService->>Database: Query latest metrics
        MetricsService->>ExternalAPI: Fetch external data
        MetricsService->>WebSocket: Send metrics update
        WebSocket->>Dashboard: Broadcast to subscribers
    end

Live Metrics Implementation

@Component
public class LiveMetricsService {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @Scheduled(fixedRate = 5000) // Every 5 seconds
    public void sendMetricsUpdate() {
        MetricsData metrics = collectCurrentMetrics();
        
        messagingTemplate.convertAndSend("/topic/metrics", metrics);
    }
    
    @Scheduled(fixedRate = 1000) // Every second for critical metrics
    public void sendCriticalMetrics() {
        CriticalMetrics critical = getCriticalMetrics();
        
        if (critical.hasAlerts()) {
            messagingTemplate.convertAndSend("/topic/alerts", critical);
        }
    }
    
    private MetricsData collectCurrentMetrics() {
        return MetricsData.builder()
            .cpuUsage(systemMetrics.getCpuUsage())
            .memoryUsage(systemMetrics.getMemoryUsage())
            .activeUsers(userService.getActiveUserCount())
            .requestsPerSecond(requestMetrics.getRequestsPerSecond())
            .timestamp(LocalDateTime.now())
            .build();
    }
}

3. Collaborative Editing

flowchart LR
    subgraph UserA ["User A"]
        A1[Edit Document]
        A2[Generate Operation]
        A3[Send via WebSocket]
    end
    
    subgraph Server ["Server"]
        S1[Receive Operation]
        S2[Transform Operation]
        S3[Apply to Document]
        S4[Broadcast to Others]
    end
    
    subgraph UserB ["User B"]
        B1[Receive Operation]
        B2[Transform Locally]
        B3[Apply to Document]
    end
    
    A3 --> S1
    S1 --> S2
    S2 --> S3
    S3 --> S4
    S4 --> B1
    B1 --> B2
    B2 --> B3

Collaborative Editing Controller

@Controller
public class CollaborativeEditingController {
    
    @MessageMapping("/document.edit/{documentId}")
    @SendTo("/topic/document/{documentId}")
    public DocumentOperation handleEdit(@DestinationVariable String documentId,
                                       DocumentOperation operation,
                                       SimpMessageHeaderAccessor headerAccessor) {
        
        String userId = (String) headerAccessor.getSessionAttributes().get("userId");
        operation.setUserId(userId);
        operation.setTimestamp(System.currentTimeMillis());
        
        // Apply operational transformation
        DocumentOperation transformedOp = operationalTransformService
            .transform(documentId, operation);
        
        // Save to document history
        documentService.applyOperation(documentId, transformedOp);
        
        return transformedOp;
    }
    
    @MessageMapping("/document.cursor/{documentId}")
    @SendTo("/topic/document/{documentId}/cursors")
    public CursorPosition updateCursor(@DestinationVariable String documentId,
                                      CursorPosition position,
                                      SimpMessageHeaderAccessor headerAccessor) {
        
        String userId = (String) headerAccessor.getSessionAttributes().get("userId");
        position.setUserId(userId);
        
        return position;
    }
}

Client-Side Implementation

JavaScript WebSocket Client

// STOMP Client Setup
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
    console.log('Connected: ' + frame);
    
    // Subscribe to public messages
    stompClient.subscribe('/topic/public', function(message) {
        const chatMessage = JSON.parse(message.body);
        displayMessage(chatMessage);
    });
    
    // Subscribe to private messages
    stompClient.subscribe('/user/queue/private', function(message) {
        const privateMessage = JSON.parse(message.body);
        displayPrivateMessage(privateMessage);
    });
});

// Send message
function sendMessage() {
    const message = {
        sender: username,
        content: messageInput.value,
        type: 'CHAT'
    };
    
    stompClient.send('/app/chat.sendMessage', {}, JSON.stringify(message));
    messageInput.value = '';
}

// Handle connection states
stompClient.onWebSocketError = function(error) {
    console.error('WebSocket Error: ', error);
    showConnectionStatus('Error: Connection lost');
};

stompClient.onStompError = function(frame) {
    console.error('STOMP Error: ', frame.headers['message']);
    showConnectionStatus('Error: ' + frame.headers['message']);
};

React WebSocket Hook

import { useEffect, useState } from 'react';
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';

export const useWebSocket = (url, subscriptions = []) => {
    const [stompClient, setStompClient] = useState(null);
    const [connected, setConnected] = useState(false);
    const [messages, setMessages] = useState([]);

    useEffect(() => {
        const socket = new SockJS(url);
        const client = Stomp.over(socket);

        client.connect({}, 
            // Success callback
            (frame) => {
                setConnected(true);
                setStompClient(client);
                
                // Subscribe to topics
                subscriptions.forEach(({ topic, callback }) => {
                    client.subscribe(topic, (message) => {
                        const parsedMessage = JSON.parse(message.body);
                        setMessages(prev => [...prev, parsedMessage]);
                        callback && callback(parsedMessage);
                    });
                });
            },
            // Error callback
            (error) => {
                console.error('WebSocket connection error:', error);
                setConnected(false);
            }
        );

        return () => {
            if (client && client.connected) {
                client.disconnect();
            }
        };
    }, [url]);

    const sendMessage = (destination, message) => {
        if (stompClient && connected) {
            stompClient.send(destination, {}, JSON.stringify(message));
        }
    };

    return {
        connected,
        messages,
        sendMessage,
        stompClient
    };
};

Security and Authentication

WebSocket Security Architecture

flowchart TB
    subgraph SecurityLayers ["Security Layers"]
        A1[CORS Configuration]
        A2[Authentication]
        A3[Authorization]
        A4[Message Filtering]
        A5[Rate Limiting]
    end
    
    Client[Client] --> A1
    A1 --> A2
    A2 --> A3
    A3 --> A4
    A4 --> A5
    A5 --> WebSocket[WebSocket Handler]

Authentication Implementation

@Configuration
public class WebSocketAuthConfig {
    
    @Bean
    public AuthChannelInterceptor authChannelInterceptor() {
        return new AuthChannelInterceptor();
    }
    
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(authChannelInterceptor());
    }
}

@Component
public class AuthChannelInterceptor implements ChannelInterceptor {
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        
        if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
            String token = accessor.getFirstNativeHeader("Authorization");
            
            if (token != null && tokenProvider.validateToken(token)) {
                String username = tokenProvider.getUsernameFromToken(token);
                UsernamePasswordAuthenticationToken auth = 
                    new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
                SecurityContextHolder.getContext().setAuthentication(auth);
                accessor.setUser(auth);
            } else {
                throw new IllegalArgumentException("Invalid token");
            }
        }
        
        return message;
    }
}

Message-Level Security

@Controller
public class SecureMessageController {
    
    @MessageMapping("/secure.message")
    @PreAuthorize("hasRole('USER')")
    @SendTo("/topic/secure")
    public SecureMessage handleSecureMessage(SecureMessage message, 
                                           Principal principal) {
        // Validate sender
        if (!message.getSender().equals(principal.getName())) {
            throw new IllegalArgumentException("Sender mismatch");
        }
        
        // Filter sensitive content
        message.setContent(contentFilter.filter(message.getContent()));
        
        return message;
    }
    
    @MessageMapping("/admin.broadcast")
    @PreAuthorize("hasRole('ADMIN')")
    @SendTo("/topic/admin")
    public AdminMessage handleAdminMessage(AdminMessage message, 
                                         Principal principal) {
        message.setSender(principal.getName());
        return message;
    }
}

Performance Optimization and Monitoring

Connection Management

flowchart TB
    subgraph ConnectionPoolMgmt ["Connection Pool Management"]
        CM[Connection Manager]
        CS[Connection Store]
        HB[Heartbeat Monitor]
        CL[Connection Limiter]
    end
    
    subgraph PerformanceMetrics ["Performance Metrics"]
        PM[Performance Monitor]
        TH[Throughput Tracking]
        LAT[Latency Measurement]
        MEM[Memory Usage]
    end
    
    WebSocket --> CM
    CM --> CS
    CM --> HB
    CM --> CL
    
    CM --> PM
    PM --> TH
    PM --> LAT
    PM --> MEM

Connection Management Service

@Component
@Slf4j
public class WebSocketConnectionManager {
    
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    private final Map<String, Set<String>> roomSessions = new ConcurrentHashMap<>();
    
    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        String sessionId = (String) event.getMessage().getHeaders().get("simpSessionId");
        log.info("WebSocket connection established: {}", sessionId);
        
        // Track connection metrics
        connectionMetrics.incrementConnections();
    }
    
    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        String sessionId = (String) event.getMessage().getHeaders().get("simpSessionId");
        String username = (String) event.getSessionAttributes().get("username");
        String roomId = (String) event.getSessionAttributes().get("roomId");
        
        if (roomId != null && username != null) {
            leaveRoom(roomId, sessionId);
            notifyUserLeft(roomId, username);
        }
        
        connectionMetrics.decrementConnections();
        log.info("WebSocket connection closed: {}", sessionId);
    }
    
    public void joinRoom(String roomId, String sessionId, String username) {
        roomSessions.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(sessionId);
        log.info("User {} joined room {}", username, roomId);
    }
    
    public void leaveRoom(String roomId, String sessionId) {
        Set<String> sessions = roomSessions.get(roomId);
        if (sessions != null) {
            sessions.remove(sessionId);
            if (sessions.isEmpty()) {
                roomSessions.remove(roomId);
            }
        }
    }
    
    public int getRoomSize(String roomId) {
        return roomSessions.getOrDefault(roomId, Collections.emptySet()).size();
    }
}

Performance Configuration

# application.yml
spring:
  websocket:
    stomp:
      message-size-limit: 64KB
      send-buffer-size-limit: 512KB
      send-time-limit: 20s
    
server:
  tomcat:
    max-connections: 10000
    max-threads: 200
    
management:
  endpoints:
    web:
      exposure:
        include: websocket, metrics
        
logging:
  level:
    org.springframework.messaging: DEBUG
    org.springframework.web.socket: DEBUG

Custom Metrics

@Component
public class WebSocketMetrics {
    
    private final Counter messagesReceived;
    private final Counter messagesSent;
    private final Gauge activeConnections;
    private final Timer messageProcessingTime;
    
    public WebSocketMetrics(MeterRegistry meterRegistry) {
        this.messagesReceived = Counter.builder("websocket.messages.received")
            .description("Total messages received via WebSocket")
            .register(meterRegistry);
            
        this.messagesSent = Counter.builder("websocket.messages.sent")
            .description("Total messages sent via WebSocket")
            .register(meterRegistry);
            
        this.activeConnections = Gauge.builder("websocket.connections.active")
            .description("Current active WebSocket connections")
            .register(meterRegistry, this, WebSocketMetrics::getActiveConnectionCount);
            
        this.messageProcessingTime = Timer.builder("websocket.message.processing.time")
            .description("Time taken to process WebSocket messages")
            .register(meterRegistry);
    }
    
    public void recordMessageReceived(String type) {
        messagesReceived.increment(Tags.of("type", type));
    }
    
    public void recordMessageSent(String destination) {
        messagesSent.increment(Tags.of("destination", destination));
    }
    
    public Timer.Sample startMessageProcessing() {
        return Timer.start(messageProcessingTime);
    }
    
    private double getActiveConnectionCount() {
        // Implementation to get active connection count
        return connectionManager.getActiveConnectionCount();
    }
}

Testing WebSocket Applications

Testing Architecture

flowchart TB
    subgraph TestingLayers ["Testing Layers"]
        UT[Unit Tests]
        IT[Integration Tests]
        E2E[End-to-End Tests]
        LT[Load Tests]
    end
    
    subgraph TestComponents ["Test Components"]
        TW[Test WebSocket Client]
        MS[Mock STOMP Server]
        TC[Test Controllers]
        TM[Test Message Handlers]
    end
    
    UT --> TC
    UT --> TM
    IT --> TW
    IT --> MS
    E2E --> TW
    LT --> TW

Integration Testing

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class WebSocketIntegrationTest {
    
    @LocalServerPort
    private int port;
    
    private StompSession stompSession;
    private final BlockingQueue<ChatMessage> blockingQueue = new ArrayBlockingQueue<>(1);
    
    @BeforeEach
    void setUp() throws Exception {
        WebSocketStompClient stompClient = new WebSocketStompClient(new SockJSClient(
            List.of(new WebSocketTransport(new StandardWebSocketClient()))));
        
        stompClient.setMessageConverter(new MappingJackson2MessageConverter());
        
        stompSession = stompClient.connect("ws://localhost:" + port + "/ws", 
            new StompSessionHandlerAdapter() {}).get(5, SECONDS);
    }
    
    @Test
    void shouldReceiveMessageAfterSending() throws Exception {
        // Subscribe to topic
        stompSession.subscribe("/topic/public", new StompFrameHandler() {
            @Override
            public Type getPayloadType(StompHeaders headers) {
                return ChatMessage.class;
            }
            
            @Override
            public void handleFrame(StompHeaders headers, Object payload) {
                blockingQueue.offer((ChatMessage) payload);
            }
        });
        
        // Send message
        ChatMessage message = new ChatMessage();
        message.setSender("testUser");
        message.setContent("Hello World");
        message.setType(MessageType.CHAT);
        
        stompSession.send("/app/chat.sendMessage", message);
        
        // Verify message received
        ChatMessage receivedMessage = blockingQueue.poll(5, SECONDS);
        
        assertThat(receivedMessage).isNotNull();
        assertThat(receivedMessage.getSender()).isEqualTo("testUser");
        assertThat(receivedMessage.getContent()).isEqualTo("Hello World");
    }
    
    @Test
    void shouldHandleMultipleConnections() throws Exception {
        // Test concurrent connections
        List<StompSession> sessions = new ArrayList<>();
        CountDownLatch latch = new CountDownLatch(5);
        
        for (int i = 0; i < 5; i++) {
            StompSession session = createTestSession();
            sessions.add(session);
            
            session.subscribe("/topic/public", new StompFrameHandler() {
                @Override
                public Type getPayloadType(StompHeaders headers) {
                    return ChatMessage.class;
                }
                
                @Override
                public void handleFrame(StompHeaders headers, Object payload) {
                    latch.countDown();
                }
            });
        }
        
        // Send broadcast message
        stompSession.send("/app/chat.sendMessage", createTestMessage());
        
        // Verify all sessions receive message
        assertTrue(latch.await(10, SECONDS));
    }
}

Load Testing with JMeter

<!-- JMeter WebSocket Test Plan -->
<TestPlan>
  <WebSocketSampler>
    <stringProp name="serverNameOrIp">localhost</stringProp>
    <stringProp name="portNumber">8080</stringProp>
    <stringProp name="path">/ws</stringProp>
    <stringProp name="connectionTimeout">5000</stringProp>
    <stringProp name="responseTimeout">10000</stringProp>
    <stringProp name="protocol">SockJS</stringProp>
    
    <!-- STOMP Connect Frame -->
    <stringProp name="connectFrame">
      CONNECT
      accept-version:1.1,1.0
      heart-beat:10000,10000
      
    </stringProp>
    
    <!-- Test Messages -->
    <stringProp name="requestData">
      SEND
      destination:/app/chat.sendMessage
      content-type:application/json
      
      {"sender":"testUser","content":"Load test message","type":"CHAT"}
    </stringProp>
  </WebSocketSampler>
</TestPlan>

Best Practices and Common Pitfalls

Best Practices Checklist

Architecture & Design

  • ✅ Choose the right approach (Raw WebSocket vs STOMP)
  • ✅ Design for scalability with external message brokers
  • ✅ Implement proper connection lifecycle management
  • ✅ Use appropriate message serialization (JSON, MessagePack)

Security

  • ✅ Implement authentication and authorization
  • ✅ Validate all incoming messages
  • ✅ Use HTTPS/WSS in production
  • ✅ Implement rate limiting to prevent abuse

Performance

  • ✅ Monitor connection counts and message throughput
  • ✅ Implement connection pooling and reuse
  • ✅ Use message batching for high-frequency updates
  • ✅ Configure appropriate buffer sizes and timeouts

Reliability

  • ✅ Handle connection drops gracefully
  • ✅ Implement reconnection logic on client side
  • ✅ Use heartbeat/ping-pong for connection health
  • ✅ Store critical messages for delivery guarantee

Common Pitfalls to Avoid

1. Memory Leaks from Session Management

// ❌ Bad: Sessions not properly cleaned up
private final Map<String, WebSocketSession> sessions = new HashMap<>();

// ✅ Good: Use concurrent collections and cleanup listeners
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
    String sessionId = event.getSessionId();
    sessions.remove(sessionId);
    // Additional cleanup...
}

2. Blocking Operations in Message Handlers

// ❌ Bad: Blocking database call in message handler
@MessageMapping("/chat.send")
public void handleMessage(ChatMessage message) {
    expensiveDatabase.save(message); // Blocks message processing
}

// ✅ Good: Async processing
@MessageMapping("/chat.send")
public void handleMessage(ChatMessage message) {
    CompletableFuture.runAsync(() -> {
        expensiveDatabase.save(message);
    });
}

3. Not Handling Backpressure

// ✅ Good: Implement backpressure handling
@Component
public class BackpressureHandler {
    
    private final Semaphore messageSemaphore = new Semaphore(1000);
    
    public boolean tryAcquireMessageSlot() {
        return messageSemaphore.tryAcquire();
    }
    
    public void releaseMessageSlot() {
        messageSemaphore.release();
    }
}

Scaling WebSocket Applications

Horizontal Scaling Architecture

flowchart TB
    subgraph LoadBalancer ["Load Balancer"]
        LB[Sticky Sessions]
    end
    
    subgraph ApplicationInstances ["Application Instances"]
        APP1[App Instance 1]
        APP2[App Instance 2]
        APP3[App Instance 3]
    end
    
    subgraph MessageBrokerCluster ["Message Broker Cluster"]
        MB1[RabbitMQ Node 1]
        MB2[RabbitMQ Node 2]
        MB3[RabbitMQ Node 3]
    end
    
    subgraph SessionStore ["Session Store"]
        REDIS[(Redis Cluster)]
    end
    
    Client --> LB
    LB --> APP1
    LB --> APP2
    LB --> APP3
    
    APP1 <--> MB1
    APP2 <--> MB2
    APP3 <--> MB3
    
    MB1 <--> MB2
    MB2 <--> MB3
    MB3 <--> MB1
    
    APP1 <--> REDIS
    APP2 <--> REDIS
    APP3 <--> REDIS

Configuration for Production

# application-prod.yml
spring:
  rabbitmq:
    host: rabbitmq-cluster.example.com
    port: 5672
    username: ${RABBITMQ_USERNAME}
    password: ${RABBITMQ_PASSWORD}
    virtual-host: /websocket
    
  redis:
    host: redis-cluster.example.com
    port: 6379
    password: ${REDIS_PASSWORD}
    timeout: 2000ms
    
  websocket:
    stomp:
      relay:
        enabled: true
        host: ${RABBITMQ_STOMP_HOST:localhost}
        port: ${RABBITMQ_STOMP_PORT:61613}
        login: ${RABBITMQ_USERNAME}
        passcode: ${RABBITMQ_PASSWORD}
        
server:
  tomcat:
    max-connections: 10000
    max-threads: 300
    
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,websocket
  metrics:
    export:
      prometheus:
        enabled: true

Summary

WebSockets in Spring Boot provide powerful real-time communication capabilities. Key takeaways:

When to Use Each Approach:

  • Raw WebSocket (@EnableWebSocket): Custom protocols, binary data, low-level control
  • STOMP WebSocket (@EnableWebSocketMessageBroker): Chat apps, publish-subscribe, room-based communication

Production Considerations:

  • Use external message brokers for scaling
  • Implement proper security and authentication
  • Monitor performance and connection health
  • Handle failures gracefully with reconnection logic
  • Test thoroughly including load testing

Architecture Patterns:

  • Publisher-Subscriber for broadcasting
  • Point-to-Point for private messaging
  • Request-Response for interactive features
  • Event-Driven for real-time updates

WebSockets enable rich, interactive applications but require careful consideration of security, scalability, and reliability patterns for production deployment.

@EnableWebSocketMessageBroker:

This is used for implementing a STOMP-based messaging system. Suitable for applications needing a publish/subscribe model or complex routing with destinations. Relies on Spring’s simp messaging (simple messaging protocol) abstraction. Example use case: A chat application with topic-based subscriptions.

@EnableWebSocket:

This is for creating raw WebSocket handlers. Allows handling low-level WebSocket frames (e.g., TextMessage, BinaryMessage) directly. Example use case: Custom WebSocket-based protocols or real-time data streams like stock prices.