Event Subscriptions with WebSockets in Spring Boot

Today·4 min read
#Spring Boot#WebSockets#gRPC#Event-driven Architecture

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:


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:

  1. Connects to /ws
  2. Subscribes to /user/queue/sync
  3. 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:

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:

  1. Stores the subscription in a ConcurrentHashMap.
  2. Logs the active subscription count.
  3. 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));
    }
}

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:

  1. Client connects → establishes WebSocket connection and subscribes.
  2. Server calls add(subscription) → stores the subscription.
  3. Event occurssendMessage() forwards the event via WebSocket.
  4. 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:

When a new chat message arrives, the backend automatically pushes it to the subscribed client in real time.


9. Why Use Subscriptions?


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.