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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
