Polling for updates wastes bandwidth and adds latency. Spring Boot WebSocket with STOMP gives you real-time bidirectional communication for notifications, dashboards, and live feeds.
JOptimize Team
Polling every 5 seconds means up to 5 seconds of latency for every update, with constant unnecessary requests. WebSocket establishes a persistent connection: the server pushes updates the instant they happen. For notifications, live dashboards, collaborative features, and order tracking — WebSocket is the right tool.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic", "/queue"); // In-memory message broker config.setApplicationDestinationPrefixes("/app"); // Client sends to /app/... } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") .setAllowedOriginPatterns("https://*.myapp.com", "http://localhost:*") .withSockJS(); // Fallback for browsers without native WebSocket } }
@Controller @RequiredArgsConstructor public class OrderWebSocketController { private final SimpMessagingTemplate messagingTemplate; // Handles messages FROM clients @MessageMapping("/orders/track") // Client sends to /app/orders/track @SendTo("/topic/orders") // Broadcast to all subscribers public OrderStatusUpdate trackOrder(TrackOrderRequest request) { Order order = orderService.findById(request.getOrderId()); return new OrderStatusUpdate(order.getId(), order.getStatus(), order.getUpdatedAt()); } // Push update from business logic (called when order status changes) public void pushOrderUpdate(Long orderId, OrderStatus newStatus) { messagingTemplate.convertAndSend( "/topic/orders/" + orderId, new OrderStatusUpdate(orderId, newStatus, LocalDateTime.now()) ); } } // Trigger push from service layer @Service @RequiredArgsConstructor public class OrderService { private final OrderWebSocketController wsController; @Transactional public void updateStatus(Long orderId, OrderStatus newStatus) { Order order = orderRepo.findById(orderId).orElseThrow(); order.setStatus(newStatus); orderRepo.save(order); wsController.pushOrderUpdate(orderId, newStatus); // Push in real-time } }
// Send to a specific user (private notifications) public void sendNotification(String userId, NotificationDto notification) { messagingTemplate.convertAndSendToUser( userId, "/queue/notifications", // User subscribes to /user/queue/notifications notification ); } // Usage: sendNotification("user-42", new NotificationDto("Your order has shipped!")); // Delivered only to sessions authenticated as user-42
@Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages .nullDestMatcher().authenticated() // Connection requires auth .simpSubscribeDestMatchers("/user/**").authenticated() .simpDestMatchers("/app/**").authenticated() .anyMessage().denyAll(); } @Override protected boolean sameOriginDisabled() { return true; } // Handle CORS separately } // Channel interceptor — validate JWT on connection @Component @RequiredArgsConstructor public class JwtChannelInterceptor implements ChannelInterceptor { private final JwtDecoder jwtDecoder; @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); if (StompCommand.CONNECT.equals(accessor.getCommand())) { String token = accessor.getFirstNativeHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { Jwt jwt = jwtDecoder.decode(token.substring(7)); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( jwt.getSubject(), null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); accessor.setUser(auth); } } return message; } }
The in-memory broker only works for a single instance. For multi-instance deployments:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
@Override public void configureMessageBroker(MessageBrokerRegistry config) { // Replace in-memory broker with Redis relay config.enableStompBrokerRelay("/topic", "/queue") .setRelayHost("redis") .setRelayPort(6379) .setClientLogin("guest") .setClientPasscode("guest"); config.setApplicationDestinationPrefixes("/app"); }
With Redis as the broker, a message sent from Instance A is delivered to subscribers connected to Instance B — horizontal scaling works.
import { Client } from '@stomp/stompjs'; import SockJS from 'sockjs-client'; const client = new Client({ webSocketFactory: () => new SockJS('/ws'), connectHeaders: { Authorization: 'Bearer ' + getJwtToken() }, onConnect: () => { // Subscribe to order updates client.subscribe('/topic/orders/42', (message) => { const update = JSON.parse(message.body); updateOrderStatusUI(update.status); }); // Subscribe to personal notifications client.subscribe('/user/queue/notifications', (message) => { showNotification(JSON.parse(message.body)); }); }, onDisconnect: () => console.log('Disconnected'), reconnectDelay: 5000, // Auto-reconnect after 5s }); client.activate();
reconnectDelay with exponential backoffSpring Boot WebSocket with STOMP enables real-time features with minimal setup: broadcast to all subscribers with @SendTo, send to specific users with convertAndSendToUser, authenticate with JWT channel interceptors, and scale with Redis relay. The result is sub-100ms update delivery instead of 5-second polling intervals — without polling overhead.
JOptimize identifies polling patterns that should be WebSocket, missing authentication on WebSocket endpoints, and in-memory broker usage in multi-instance configurations.
Replace polling with real-time push — free architecture scan.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.