Build a Millisecond‑Level Real‑Time Online System with Spring Boot, WebSocket, and Redis

This article demonstrates how to create a millisecond‑level real‑time online user tracking system using Spring Boot 3.5, WebSocket with STOMP, and Redis pub/sub, covering environment setup, Maven dependencies, server‑side configuration, presence services, event listeners, and a simple front‑end page.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Build a Millisecond‑Level Real‑Time Online System with Spring Boot, WebSocket, and Redis

Introduction

Real‑time online status is a core requirement for many applications, from collaborative tools to social platforms. Traditional polling suffers from high latency and poor performance, while a WebSocket‑based push combined with Redis’s high‑throughput pub/sub can achieve millisecond‑level synchronization.

Environment

Spring Boot version 3.5.0 is used for the implementation.

1. Maven Dependencies

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2. WebSocket Configuration

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/handshake")
                .setHandshakeHandler(new DefaultHandshakeHandler() {
                    @Override
                    protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
                                                    Map<String, Object> attributes) {
                        String user = request.getURI().getQuery();
                        if (user != null && user.startsWith("user=")) {
                            String username = user.substring(5);
                            return new StompPrincipal(username);
                        }
                        return null;
                    }
                })
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic", "/queue");
    }

    private static class StompPrincipal implements Principal {
        private final String name;
        public StompPrincipal(String name) { this.name = name; }
        @Override public String getName() { return this.name; }
    }
}

3. Redis Pub/Sub Configuration

@Configuration
public class RedisConfig {
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                          MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, new ChannelTopic("presence-events"));
        return container;
    }

    @Bean
    MessageListenerAdapter listenerAdapter(PresenceSubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, "onMessage");
    }

    @Bean
    ChannelTopic presenceTopic() { return new ChannelTopic("presence-events"); }
}

4. Presence Service (Online State Management)

@Service
public class PresenceService {
    private static final String ONLINE_SET = "presence:online";
    private static final String LAST_SEEN = "presence:lastSeen:";
    private static final String SESSIONS = "presence:sessions:";

    private final StringRedisTemplate stringRedisTemplate;
    private final PresencePublisher publisher;
    private final PresenceBroadcast broadcast;

    public PresenceService(StringRedisTemplate stringRedisTemplate, PresencePublisher publisher,
                           PresenceBroadcast broadcast) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.publisher = publisher;
        this.broadcast = broadcast;
    }

    public void applyPresenceUpdate(PresenceEvent event) {
        if (event.online()) {
            stringRedisTemplate.opsForSet().add(ONLINE_SET, event.userId());
        } else {
            stringRedisTemplate.opsForSet().remove(ONLINE_SET, event.userId());
            stringRedisTemplate.opsForValue().set(LAST_SEEN + event.userId(), String.valueOf(event.timestamp()));
        }
        this.broadcast.pushUpdate(event);
    }

    public void onConnect(String userId) {
        long count = stringRedisTemplate.opsForValue().increment(SESSIONS + userId);
        if (count == 1) { publishPresence(userId, true); }
    }

    public void onDisconnect(String userId) {
        long count = stringRedisTemplate.opsForValue().decrement(SESSIONS + userId);
        if (count <= 0) {
            publishPresence(userId, false);
            stringRedisTemplate.delete(SESSIONS + userId);
        }
    }

    private void publishPresence(String userId, boolean online) {
        PresenceEvent event = new PresenceEvent(userId, online, System.currentTimeMillis(), UUID.randomUUID().toString());
        publisher.publish(event);
        this.broadcast.pushUpdate(event);
    }
}

5. Message Publisher and Broadcast

@Service
public class PresenceBroadcast {
    private final SimpMessagingTemplate messaging;
    public PresenceBroadcast(SimpMessagingTemplate messaging) { this.messaging = messaging; }
    public void pushUpdate(PresenceEvent event) {
        messaging.convertAndSend("/topic/presence", event);
    }
}

6. WebSocket Event Listeners

@Component
public class WebSocketConnectListener implements ApplicationListener<SessionConnectEvent> {
    private final PresenceService presenceService;
    public WebSocketConnectListener(PresenceService presenceService) { this.presenceService = presenceService; }
    @Override
    public void onApplicationEvent(SessionConnectEvent event) {
        String userId = event.getUser().getName();
        presenceService.onConnect(userId);
    }
}

@Component
public class WebSocketDisconnectListener implements ApplicationListener<SessionDisconnectEvent> {
    private final PresenceService presenceService;
    public WebSocketDisconnectListener(PresenceService presenceService) { this.presenceService = presenceService; }
    @Override
    public void onApplicationEvent(SessionDisconnectEvent event) {
        String userId = event.getUser().getName();
        presenceService.onDisconnect(userId);
    }
}

7. Front‑End Page

<head>
  <script>
    let stompClient = null;
    let name = null;
    function connect() {
      name = document.querySelector("#username").value;
      const socket = new SockJS(`/handshake?user=${name}`);
      stompClient = webstomp.over(socket, { debug: true });
      stompClient.connect({ login: "pack" }, function(frame) {
        if (frame.command === 'CONNECTED') {
          addOnlineUser(name);
          stompClient.subscribe('/topic/presence', data => {
            const body = JSON.parse(data.body);
            if (body.online) { addOnlineUser(body.userId); }
            else { deleteUser(body.userId); }
          });
        }
      });
    }
    function disconnect() {
      if (stompClient) {
        stompClient.disconnect(() => {
          stompClient = null;
          if (name) deleteUser(name);
        });
      }
    }
    function addOnlineUser(name) {
      if (document.querySelector(`li[data-name="${name}"]`)) return;
      const li = document.createElement('li');
      li.setAttribute('data-name', name);
      li.textContent = name;
      document.querySelector('.users').appendChild(li);
    }
    function deleteUser(name) {
      const li = document.querySelector(`li[data-name="${name}"]`);
      if (li) li.remove();
    }
  </script>
</head>
<body>
  <h1>Online User Statistics</h1>
  <div class="input-group">
    <label for="username">Login User</label>
    <input type="text" id="username" placeholder="Enter username"/>
  </div>
  <div class="button-group">
    <button id="connectBtn" onclick="connect()">Connect</button>
    <button id="disconnectBtn" onclick="disconnect()">Disconnect</button>
  </div>
  <span class="online-title">Current Online Users</span>
  <div class="online-list">
    <ul class="users"></ul>
  </div>
</body>

8. Result

The page shows a list that updates instantly as users connect or disconnect, demonstrating millisecond‑level online presence tracking.

Javareal-timeRedisSpring BootWebSocketspring-messagingonline-presence
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.