Build Real‑Time Server‑Sent Events with Spring Boot 3 and Reactive Sinks

This article demonstrates how to create a complete real‑time message broadcasting system using Spring Boot 3's reactive Server‑Sent Events (SSE) and WebFlux, covering the core Sink component, backend implementation, and a simple front‑end page to display live updates.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Build Real‑Time Server‑Sent Events with Spring Boot 3 and Reactive Sinks

Introduction

Real‑time message push is increasingly needed in web applications such as stock quotes or chat rooms. Spring Boot 3’s reactive Server‑Sent Events (SSE) combined with WebFlux provides an efficient solution.

What are Sinks?

Sinks act as both publisher and subscriber. They allow multiple data streams to emit elements that can be observed as a Flux by subscribers.

Key features

Publisher : use emit or tryEmit methods to push data.

Subscriber : retrieve data via asFlux() or asMono().

How Sinks work

Generating data : call tryEmitNext(), tryEmitComplete() or tryEmitError().

Consuming data : subscribers connect with asFlux() or asMono().

Sink types

Sinks.Many : can emit multiple elements.

unicast() – single subscriber

multicast() – multiple subscribers

replay() – new subscribers receive the latest element.

Sinks.One : emits a single element.

Practical example

1. Define the message model

public class Message {
  private Integer id;
  private String author;
  private String time;
  private String message;
  // getters, setters
}

2. Configure a replay sink

@Configuration
public class SinkConfig {
  @Bean
  Sinks.Many<Message> sink() {
    return Sinks.many().replay().limit(1);
  }
}

3. Service for publishing and subscribing

@Service
public class MessageService {
  private final Sinks.Many<Message> messageSink;
  public MessageService(Sinks.Many<Message> messageSink) {
    this.messageSink = messageSink;
  }
  public Mono<Message> saveMessage(Mono<Message> message) {
    return message.doOnNext(messageSink::tryEmitNext);
  }
  public Flux<Message> messageStream() {
    return messageSink.asFlux();
  }
}

4. REST controller

@RestController
@RequestMapping("/messages")
public class MessageController {
  private final AtomicInteger count = new AtomicInteger();
  private final MessageService messageService;
  public MessageController(MessageService messageService) {
    this.messageService = messageService;
  }
  @GetMapping("/send")
  public Mono<Message> sendMessage(String message) {
    String time = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
                    .format(LocalDateTime.now());
    Message msg = new Message(count.incrementAndGet(), "Pack", time, message);
    return messageService.saveMessage(Mono.just(msg));
  }
  @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  public Flux<Message> messageStream() {
    return messageService.messageStream();
  }
}

5. Front‑end page

<div class="container">
  <h1>Real‑time Messages</h1>
  <div class="controls">
    <div class="buttons">
      <button id="startBtn">Start Listening</button>
      <button id="stopBtn" disabled>Stop Listening</button>
    </div>
  </div>
  <table>
    <thead>
      <tr><th>ID</th><th>Author</th><th>Time</th><th>Content</th></tr>
    </thead>
    <tbody id="messages"></tbody>
  </table>
</div>

6. CSS (simplified)

body {font-family:'Roboto',sans-serif;margin:0;background:#f5f5f5;display:flex;justify-content:center;align-items:flex-start;padding:20px;}
.container {width:90%;max-width:1000px;margin:auto;text-align:center;}
h1 {font-size:2rem;color:#4285f4;margin-bottom:20px;}
button {padding:8px 16px;background:#4285f4;color:white;border:none;border-radius:5px;cursor:pointer;}
button:disabled {background:#ccc;cursor:not-allowed;}
table {width:100%;border-collapse:collapse;background:white;box-shadow:0 4px 12px rgba(0,0,0,0.1);border-radius:8px;}
th, td {padding:15px;text-align:left;border-bottom:1px solid #ddd;}

7. JavaScript for SSE

let eventSource;
const messageContainer = document.getElementById('messages');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');

function startStream() {
  const url = `http://localhost:8080/messages/stream`;
  eventSource = new EventSource(url);
  eventSource.onmessage = event => {
    const data = JSON.parse(event.data);
    const row = `<tr>
      <td>${data.id}</td>
      <td>${data.author}</td>
      <td>${data.time}</td>
      <td>${data.message}</td>
    </tr>`;
    messageContainer.insertAdjacentHTML('afterbegin', row);
  };
  startBtn.disabled = true;
  stopBtn.disabled = false;
}
function stopStream() {
  if (eventSource) {
    eventSource.close();
    startBtn.disabled = false;
    stopBtn.disabled = true;
  }
}
startBtn.addEventListener('click', startStream);
stopBtn.addEventListener('click', stopStream);

Result

The page displays incoming messages instantly without polling, demonstrating a complete end‑to‑end SSE solution with Spring Boot 3.

图片
图片
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendJavaSpring BootreactiveWebFluxServer-Sent EventsSinks
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.