Implementing Server-Sent Events with Spring Boot

Server-Sent Events (SSE) is a web technology that enables a server to send real-time updates to web clients. In contrast to WebSockets, which is bidirectional, SSE is a simple, unidirectional communication channel where the server pushes data to the client. This makes SSE more straightforward and efficient for use cases where only the server needs to send data to the client. In this blog post, we will delve into how to implement Server-Sent Events using Spring Boot.

How does it work?

When a client wants to receive updates, it opens a connection to the server. Instead of closing the connection after sending a response, like in traditional HTTP communication, the server keeps the connection open. This allows the server to send updates to the client whenever there's new data available. These updates are sent as messages, each of which is a small chunk of data.

Why use SSE?

SSE is perfect for use cases where the server needs to push updates to the client in real-time, but the client doesn't need to send data to the server in the same manner. It's simpler and more straightforward than other real-time data transmission technologies like WebSockets, as it operates over traditional HTTP.

One key advantage of SSE over techniques like polling (where the client periodically checks for new data) is that SSE provides updates in real-time without any delay that could be caused by the polling interval. Plus, it's more efficient as it reduces unnecessary network traffic.

Getting Started

Creating a Spring Boot Project

Create a new project using Spring Initializer, either through your IDE or by visiting start.spring.io. Select the "Web" dependency and generate the project. Extract the downloaded zip file and open the project in your IDE.

Open the pom.xml file and you should have the following dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Implementing Server-Sent Events

Now let's create a rest controller that will allow our clients to subscribe to these events.

package com.example.ssewithspringboot.controller;

import org.springframework.web.bind.annotation.RestController;

@Tag(name = "notifications", description = "Notifications operations")
@RestController
@RequestMapping("/api/notifications")
public class NotificationsController {
    private final EventHandlerAdapter eventHandlerAdapter;

    public NotificationsController(EventHandlerAdapter eventHandlerAdapter) {
        this.eventHandlerAdapter = eventHandlerAdapter;
    }

    @GetMapping(value = "/subscribe/{userId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter subscribe(@PathVariable final Long userId) {
        SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);
        sendSubscribedEvent(sseEmitter);
        eventHandlerAdapter.registerSseEmitter(userId, sseEmitter);
        sseEmitter.onCompletion(() -> eventHandlerAdapter.unregisterSseEmitter(userId));
        sseEmitter.onTimeout(() -> eventHandlerAdapter.unregisterSseEmitter(userId));
        sseEmitter.onError((e) -> eventHandlerAdapter.unregisterSseEmitter(userId));

        return sseEmitter;
    }   

    private void sendSubscribedEvent(final SseEmitter sseEmitter) {
        try {
            sseEmitter.send(SseEmitter.event().name("Subscribed successfully!"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
@Component
public class EventHandlerAdapter implements EventHandlerPort {
    private static final Logger logger = LoggerFactory.getLogger(EventHandlerAdapter.class);
    private final NotificationsService notificationsService;
    private Map<Long, SseEmitter> sseEmitters = new HashMap<>();

    public EventHandlerAdapter(final NotificationsService notificationsService) {
        this.notificationsService = notificationsService;
    }

    @Override
    public void processEvent(NotificationEvent event) {

        logger.info("Processing  Event of type: {}", event.getType());

        if (event.getRecipients() != null) {
            Set<Long> recipients = new HashSet<>(event.getRecipients()); // ignore duplicates
            createNotifications(event, recipients);
        }
        logger.info("Finished processing Event of type: {}", event.getType());
    }

    @Override
    public void emitEvent(Long userId, final Object event) {
        final SseEmitter sseEmitter = sseEmitters.get(userId);
        if (sseEmitter != null) {
            try {
                sseEmitter.send(SseEmitter.event().name("new_notification").data(event));
            } catch (IOException e) {
                sseEmitter.complete();
                unregisterSseEmitter(userId);
            }
        }
    }

    public void registerSseEmitter(Long userId, SseEmitter sseEmitter) {
        this.sseEmitters.put(userId, sseEmitter);
    }

    public void unregisterSseEmitter(Long userId) {
        this.sseEmitters.remove(userId);
    }

    private void createNotifications(NotificationEvent event, Set<Long> recipients) {
        recipients.forEach(recipient -> {
            Notification notification = NotificationFactory.createNotification(event.getType(), event.getMetadata(), recipient);

            final Notification createdNotification = notificationsService.createNotification(notification);
            emitEvent(recipient, NotificationDTO.fromDomainModel(createdNotification));
            logger.info("Notification created successfully for recipient: {}", recipient);
        });
    }

Conclusion

Server-Sent Events are a powerful tool for enabling real-time, one-way communication from the server to the client. They are simple, efficient, and operate over traditional HTTP, making them a go-to choice for developers when real-time updates are needed.

Remember, while SSE is excellent for one-way real-time communication, if your application requires bi-directional communication (both client and server sending data in real-time), WebSockets might be a better fit. It's all about picking the right tool for the job!

Did you find this article valuable?

Support Adrian Kodja by becoming a sponsor. Any amount is appreciated!