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
public class SlidingLogRateLimiter { private final int maxRequests; private final long windowMillis;
How a sliding-log rate limiter works
rate-limiting
concurrency
sliding-window
Advanced
8 steps
go
package theme import ( "fmt"
Per-tenant HTML templates with Gin's renderer
multi-tenancy
concurrency
html-templates
Advanced
8 steps
php
<?php namespace App\Http\Controllers;
Streaming a filtered CSV export in Laravel
streaming
csv-export
query-builder
Intermediate
9 steps
python
import time import threading from enum import Enum from functools import wraps
Building a circuit breaker in Python
circuit-breaker
resilience
decorators
Advanced
7 steps
java
public final class Slugifier { private static final Pattern NON_LATIN = Pattern.compile("[^\\w-]"); private static final Pattern WHITESPACE = Pattern.compile("[\\s]+");
Building a URL slugifier in Java
regex
unicode-normalization
string-processing
Intermediate
8 steps
typescript
import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { catchError, filter, take, switchMap } from 'rxjs/operators';
Refreshing auth tokens in an Angular interceptor
http-interceptor
token-refresh
rxjs
Advanced
8 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.