Where the Problem Started

When moving from HTTP APIs to WebSocket, the first change is not syntax but mindset. Instead of one request and one response, the application keeps a connection open and exchanges events over it. This post records a small Spring Boot WebSocket implementation and the connection flow behind it.

WebSocket is a bidirectional, full-duplex communication protocol built on a persistent TCP connection.

The definition is dense, so it helps to split it into smaller parts.

Persistent means the connection keeps being maintained. Unlike REST or gRPC, where the connection ends after a single request, the connection between client and server remains open for a certain period after connection.

Bi-directional means both sides, the client and server side, can each handle traffic. Unlike normal HTTP, where the client usually sends a request and the server sends a response, in a bi-directional connection the server can send messages to the client that were not requested, meaning they are not in response format. This can be useful for notifications, live feeds, and similar features.

Full-duplex means one party can send and receive messages at the same time. The server can send and receive messages in a given time window, and the client can do the same. This lets a two-way connection perform simultaneous and dynamic data exchange. In HTTP, when the client sends a request to the server, it has to wait until the server processes it and sends a response. The server also cannot send a response before a request arrives, so HTTP can be called half-duplex.

In other words, after connection, WebSocket has no particular condition applied to message exchange.

Since WebSocket is a TCP connection, it starts with a handshake. The client first sends a normal HTTP request to the server, and at this point the header must contain upgrade : Websocket. This changes the protocol from HTTP to WebSocket.

Implementation Path

GET /websocket HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

Because a Websocket connection does not use the http:// scheme, it uses an endpoint that starts with ws:// or wss://. Like https, wss is used for a secure Websocket connection.

When closing the connection, either the client or server sends a “close frame”. This contains a status code.

1000: Normal closure.
1001: Going away (like a server shutting down or a browser tab closing).
1002: Protocol error.
1006: Abnormal closure (no close frame received).

The responding party sends its own close frame. Once both frames have been sent, the TCP connection is closed, and both parties must ensure that every resource associated with the connection has been properly released.

Here is a simple WebSocket config.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler(), "/websocket")
                .setAllowedOrigins("*");

    }
    @Bean
    org.springframework.web.socket.WebSocketHandler webSocketHandler() {
        return new WebSocketHandler();
    }
}

registerWebSocketHandlers is overridden from WebSocketConfigurer and maps a WebSocket handler to a specific URL. In this example it maps the handler to /websocket, and setAllowedOrigins("*") adds a permissive CORS rule that accepts connections from any origin.

Next is the WebSocketHandler registered as a Bean.

@Slf4j
public class WebSocketHandler implements org.springframework.web.socket.WebSocketHandler {

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.info("Connection established on session: {}", session.getId());

    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String tutorial = (String) message.getPayload();
        log.info("Message: {}", tutorial);
        session.sendMessage(new TextMessage("Started processing tutorial: " + session + " - " + tutorial));
        Thread.sleep(1000);
        session.sendMessage(new TextMessage("Completed processing tutorial: " + tutorial));

    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        log.info("Exception occured: {} on session: {}", exception.getMessage(), session.getId());

    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        log.info("Connection closed on session: {} with status: {}", session.getId(), closeStatus.getCode());

    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

}

Since I am not going to receive messages on the client side, I added the Slf4j annotation for logging. I overrode several methods in WebSocketHandler, such as message handling and connection start/end handling. In the handleMessage method, when a payload comes in, it immediately sends one message to the client, then sends one more message after one second. This is where some server-side process would go.

A simple Postman test is enough to verify the connection.

How I Verified It

Implementing a Simple WebSocket in Spring Boot screenshot 01

After the connection succeeds, sending "test message" returns one message immediately and another after one second because Thread.sleep was added in the handler.

Implementing a Simple WebSocket in Spring Boot screenshot 02

Realtime Design Takeaway

WebSocket is not simply a switch for realtime features. Connection count, session management, message routing, and state sharing across scaled instances all become part of the design. Understanding the connection model through a small example makes later STOMP, broker, and multi-instance work much easier to reason about.

The logs are also printed normally.