java 95 lines · 3 tabs

Push Live Notifications with Server-Sent Events in Spring Boot

Shared by codesnips Aug 2026
3 tabs
@RestController
@RequestMapping("/api/notifications")
public class NotificationController {

    private final SseEmitterRegistry registry;
    private final NotificationService service;

    public NotificationController(SseEmitterRegistry registry, NotificationService service) {
        this.registry = registry;
        this.service = service;
    }

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter stream(@AuthenticationPrincipal CurrentUser user) {
        SseEmitter emitter = registry.subscribe(user.getId());
        try {
            emitter.send(SseEmitter.event().name("connected").data(Map.of("userId", user.getId())));
        } catch (IOException ex) {
            emitter.completeWithError(ex);
        }
        return emitter;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.ACCEPTED)
    public void publish(@AuthenticationPrincipal CurrentUser user,
                        @Valid @RequestBody NotificationRequest request) {
        service.deliver(user.getId(), request.getTargetUserId(), request.getMessage());
    }
}
3 files · java Explain with highlit

This snippet shows how live user notifications are pushed to browsers using Server-Sent Events (SSE) in a plain Spring MVC application, without a full WebSocket stack. SSE is a good fit here because notifications flow one way (server to client), rely on a long-lived HTTP response, and get automatic reconnection from the browser's EventSource for free.

The SseEmitterRegistry tab is the heart of the design. It holds a ConcurrentHashMap keyed by user id, where each value is a CopyOnWriteArrayList<SseEmitter> so one user can have several open tabs or devices. On subscribe, it constructs an SseEmitter with a long timeout and wires onCompletion, onTimeout, and onError callbacks so the emitter removes itself from the registry when the connection drops. This self-cleanup is the critical part: without it, dead emitters accumulate and every future send throws. The send method iterates a user's emitters, calls emitter.send(...) with a named event and payload, and on IOException marks the emitter stale so it is pruned. CopyOnWriteArrayList makes concurrent iteration during a broadcast safe against subscribers connecting or disconnecting mid-loop.

The NotificationController tab exposes two endpoints. The stream handler returns an SseEmitter with produces = text/event-stream; Spring keeps the request thread's response open and streams asynchronously. It immediately sends a small connected event so clients and proxies confirm the pipe is live and the browser resets its retry timer. The publish endpoint accepts a notification body and fans it out through the registry to the target user.

The NotificationService tab sits between domain logic and transport: it persists the notification, then calls registry.send so delivery is decoupled from any single request. Because SSE only reaches currently-connected clients, persistence ensures a user who was offline still sees the notification on next load.

A few trade-offs matter. SSE runs over one HTTP connection per client, so heavy fan-out benefits from an async request timeout and enough server threads or a reactive stack. Proxies may buffer text/event-stream, so disabling response buffering is often required. For multi-instance deployments this in-memory registry must be backed by a shared bus like Redis pub/sub, since an emitter only exists on the node that accepted the connection. This pattern is ideal for dashboards, toasts, and progress feeds where WebSockets would be overkill.


Related snips

Share this code

Here's the card — post it anywhere.

Push Live Notifications with Server-Sent Events in Spring Boot — share card
Link copied