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

Walkthrough

Space play step click any line
Three takeaways
  1. 1SseEmitter keeps a one-way HTTP connection open so the server can push events until it completes or times out.
  2. 2Registering completion, timeout, and error callbacks is essential to prevent leaking dead connections from your registry.
  3. 3Decoupling delivery from creation via an application event lets any part of the system fan out messages to connected clients.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Server-Sent Events per user in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code