java
60 lines · 8 steps
Server-Sent Events per user in Spring
A Spring controller that streams live notifications to each user over SSE, tracking open connections in a thread-safe map.
Explained by
highlit
1@RestController
2@RequestMapping("/api/notifications")
3public class NotificationStreamController {
4
5 private final Map<String, CopyOnWriteArrayList<SseEmitter>> subscribers = new ConcurrentHashMap<>();
6
7 @GetMapping(value = "/stream/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
8 public SseEmitter subscribe(@PathVariable String userId) {
9 SseEmitter emitter = new SseEmitter(Duration.ofMinutes(30).toMillis());
10
11 subscribers.computeIfAbsent(userId, key -> new CopyOnWriteArrayList<>()).add(emitter);
12
13 emitter.onCompletion(() -> removeEmitter(userId, emitter));
14 emitter.onTimeout(() -> {
15 emitter.complete();
16 removeEmitter(userId, emitter);
17 });
18 emitter.onError(ex -> removeEmitter(userId, emitter));
19
20 try {
21 emitter.send(SseEmitter.event()
22 .name("connected")
23 .data(Map.of("userId", userId, "since", Instant.now().toString())));
24 } catch (IOException ex) {
25 emitter.completeWithError(ex);
26 }
27
28 return emitter;
29 }
30
31 @EventListener
32 public void onNotification(NotificationCreatedEvent event) {
33 CopyOnWriteArrayList<SseEmitter> emitters = subscribers.get(event.userId());
34 if (emitters == null) {
35 return;
36 }
37 for (SseEmitter emitter : emitters) {
38 try {
39 emitter.send(SseEmitter.event()
40 .id(event.id())
41 .name("notification")
42 .reconnectTime(3000)
43 .data(event.payload(), MediaType.APPLICATION_JSON));
44 } catch (IOException ex) {
45 emitter.completeWithError(ex);
46 removeEmitter(event.userId(), emitter);
47 }
48 }
49 }
50
51 private void removeEmitter(String userId, SseEmitter emitter) {
52 CopyOnWriteArrayList<SseEmitter> emitters = subscribers.get(userId);
53 if (emitters != null) {
54 emitters.remove(emitter);
55 if (emitters.isEmpty()) {
56 subscribers.remove(userId);
57 }
58 }
59 }
60}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1SseEmitter keeps a one-way HTTP connection open so the server can push events until it completes or times out.
- 2Registering completion, timeout, and error callbacks is essential to prevent leaking dead connections from your registry.
- 3Decoupling delivery from creation via an application event lets any part of the system fan out messages to connected clients.
Related explainers
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/server-sent-events-per-user-in-spring-explained-java-22d1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.