Event Subscriptions with WebSockets in Spring Boot
In modern event-driven architectures, subscriptions play a crucial role in delivering real-time updates to clients. Whether you are using WebSockets or gRPC, the subscription mechanism ensures that events reach the right client session.
In this blog, we’ll walk through how to set up WebSockets in Spring Boot, create subscriptions, and use them to send events to clients.
1. Enabling WebSockets in Spring Boot
Spring Boot makes it easy to set up WebSocket endpoints with STOMP support. First, we configure a WebSocket broker:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue", "/topic"); // Broker destinations
config.setUserDestinationPrefix("/user"); // For user-specific queues
config.setApplicationDestinationPrefixes("/app"); // Client -> Server
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}👉 This creates:
- A WebSocket endpoint at
/ws - A broker that lets clients subscribe to
/topic/*or/user/queue/*
2. Subscribing from the Client
On the frontend, a client can use SockJS + STOMP to connect and subscribe:
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
console.log('Connected to WebSocket');
// Subscribe to personal event queue
stompClient.subscribe('/user/queue/sync', (message) => {
const event = JSON.parse(message.body);
console.log('Received event:', event);
});
// Optionally send a subscription request
stompClient.send("/app/subscribe", {}, JSON.stringify({ subscriptionId: "sub-123" }));
});Now the client:
- Connects to
/ws - Subscribes to
/user/queue/sync - Starts receiving events whenever the server pushes messages with
simpMessagingTemplate.convertAndSendToUser()
3. What is a Subscription?
A subscription represents a client’s intent to receive real-time updates. It contains:
- ID → unique identifier for the subscription.
- Principal → the authenticated user.
- SessionId → WebSocket session ID.
- gRPCStreamObserver (optional) → for gRPC clients.
Essentially, a subscription is a contract: “Send me events as they happen.”
4. Subscription Creation
The core subscription logic lives in the SubscriptionServiceImpl class:
@Override
public Message add(Subscription subscription) {
// Store subscription in memory
store.put(subscription.getId(), subscription);
log.debug("Current alive subscriptions count: {}", store.keySet().size());
Map<String, Object> payload = new HashMap<>();
payload.put(Constants.MESSAGE, "Successfully registered for syncing events");
// Return ACK
return Message.builder()
.type(MessageType.ACK)
.payload(payload)
.build();
}👉 When a client subscribes, the server:
- Stores the subscription in a ConcurrentHashMap.
- Logs the active subscription count.
- Returns an acknowledgment message confirming the subscription.
5. Sending Events to Subscribed Clients
Once subscribed, events can be delivered through WebSockets or gRPC. The logic lives in SubscriptionConnectorImpl:
@Override
public void sendMessage(Event event, Subscription subscription) {
if (subscription.getGRPCStreamObserver() == null) {
// WebSocket delivery
simpMessagingTemplate.convertAndSendToUser(
subscription.getPrincipal().getName(),
Constants.SYNC_QUEUE_DESTINATION,
buildEventMessage(event),
createHeaders(subscription.getSessionId()));
} else {
// gRPC delivery
subscription.getGRPCStreamObserver().onNext(buildEventProtoBuf(event));
}
}- WebSocket clients → receive messages using Spring’s
SimpMessagingTemplate. - gRPC clients → receive events over
StreamObserver.
This abstraction allows you to use the same subscription logic for multiple protocols.
6. Managing Subscriptions
To keep the system clean, expired or unauthenticated subscriptions must be removed. This is handled in removeExpiredSubscriptions():
private void removeExpiredSubscriptions() {
Iterator<Map.Entry<String, Subscription>> it = store.entrySet().iterator();
while (it.hasNext()) {
Subscription subscription = it.next().getValue();
if (subscription != null && !subscription.isAuthenticated()) {
log.info("Removing subscription: {} as it is expired", subscription.getId());
it.remove();
}
}
}7. End-to-End Flow
Here’s how the subscription lifecycle works:
- Client connects → establishes WebSocket connection and subscribes.
- Server calls
add(subscription)→ stores the subscription. - Event occurs →
sendMessage()forwards the event via WebSocket. - Client disconnects or expires → subscription is removed.
8. Example Chat Application Flow
Suppose we have a chat application. A user subscribes to "/user/queue/sync". The backend:
- Calls
SubscriptionService.add(). - Stores the subscription.
- Uses
simpMessagingTemplate.convertAndSendToUser()to push messages directly to that user.
When a new chat message arrives, the backend automatically pushes it to the subscribed client in real time.
9. Why Use Subscriptions?
- Real-time updates → no polling required.
- Protocol agnostic → works for WebSocket and gRPC.
- Scalable → expired subscriptions are cleaned automatically.
Conclusion
By combining WebSocket configuration, SubscriptionServiceImpl (for managing subscriptions), and SubscriptionConnectorImpl (for delivering messages), you get a clean and extensible subscription mechanism. This allows Spring Boot applications to send real-time events over WebSockets (and even gRPC) with minimal additional code.
👉 Subscriptions act as the bridge between your event source and clients, ensuring every event reaches the right session reliably.