Spring Boot | Server-sent events (SSE)

I will show you how to use server-sent events in your Spring Boot application in this article.
Introduction
Before going into details, let’s talk about theory. The SSE is not a new fancy approach; it has been here since 2004. The first browser that started to support SSE was Opera in 2006. Now, all modern browsers support this technology. How is SSE different from the hit-and-get approach from the REST? In REST API, we hit some methods and get back some responses. In the server sents events, the consumer of REST API subscribes to events from the server.
The server-sent events are text data streams encoded in UTF-8. The data format is key-value pairs — the server-sent events to offer only uni-directional communication. Meanwhile, WebSockets offer bi-directional (full duplex) communication.
Implementation
I will use Spring Boot WebFlux in this article, but SSE can also be implemented with Spring MVC. Let’s start with a simple endpoint that will return the current traffic situation on the road every two seconds. Here is the implementation of the traffic service.
package io.vrnsky.serversenteventsdemo;
import org.springframework.stereotype.Service;
import java.util.Random;
@Service
public class TrafficService {
private static final Random RANDOM = new Random();
public String getTrafficStatus() {
int random = RANDOM.nextInt(10 - 1) + 1;
if (random < 3) {
return "light";
} else if (random <= 5) {
return "moderate";
}
return "heavy";
}
}
Now, let’s build an endpoint to allow consumers to subscribe to events.
package io.vrnsky.serversenteventsdemo;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.time.LocalTime;
@RestController
public class EventController {
private final TrafficService trafficService;
public EventController(TrafficService trafficService) {
this.trafficService = trafficService;
}
@GetMapping(path = "/traffic", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamFlux() {
return Flux.interval(Duration.ofSeconds(2))
.map(sequence -> String.format("%s at the time %s", trafficService.getTrafficStatus(), LocalTime.now()));
}
}
Now, let’s run our service and check that it works correctly.
% curl http://localhost:8080/traffic data:moderate at the time 13:43:13.339864 data:heavy at the time 13:43:15.339383 data:heavy at the time 13:43:17.335435 data:moderate at the time 13:43:19.338967 data:heavy at the time 13:43:21.335299 data:moderate at the time 13:43:23.337038 data:light at the time 13:43:25.334921
As you may see, we got events every two seconds, as expected. However, the format of events could be more convenient for the consumer since it is not JSON or XML. Let’s fix it.
First, let’s describe our event as a Java record.
package io.vrnsky.serversenteventsdemo;
import java.time.LocalTime;
public record TrafficStatus(
String status,
LocalTime time
) {
}
Then, we need to adjust our TrafficService implementation to return the correct object.
package io.vrnsky.serversenteventsdemo;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Random;
@Service
public class TrafficService {
private static final Random RANDOM = new Random();
public TrafficStatus getTrafficStatus() {
int random = RANDOM.nextInt(10 - 1) + 1;
if (random < 3) {
return new TrafficStatus("light", LocalTime.now());
} else if (random <= 5) {
return new TrafficStatus("moderate", LocalTime.now());
}
return new TrafficStatus("heavy", LocalTime.now());
}
}
Last but not least, we have to add some modifications to our endpoint.
package io.vrnsky.serversenteventsdemo;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
@RestController
public class EventController {
private final TrafficService trafficService;
public EventController(TrafficService trafficService) {
this.trafficService = trafficService;
}
@GetMapping(path = "/traffic")
public Flux<ServerSentEvent<TrafficStatus>> streamFlux() {
return Flux.interval(Duration.ofSeconds(2))
.map(sequence -> ServerSentEvent.<TrafficStatus>builder()
.id(String.valueOf(sequence))
.event("traffic-event")
.data(trafficService.getTrafficStatus())
.build());
}
}
After our service is up and running, we may again subscribe to events and should see something like this.
curl http://localhost:8080/traffic
id:0
event:traffic-event
data:{"status":"heavy","time":"13:50:57.245554"}
id:1
event:traffic-event
data:{"status":"light","time":"13:50:59.243631"}
id:2
event:traffic-event
data:{"status":"moderate","time":"13:51:01.243727"}
id:3
event:traffic-event
data:{"status":"moderate","time":"13:51:03.240843"}
id:4
event:traffic-event
data:{"status":"light","time":"13:51:05.242545"}
id:5
event:traffic-event
data:{"status":"heavy","time":"13:51:07.240015"}
id:6
event:traffic-event
data:{"status":"moderate","time":"13:51:09.240195"}
Conclusion
The server-sent event is a powerful technology that enables developers to build more responsive services. The approach with SSE is different from classical REST endpoints with the hit-and-get method. The SSE can be good choice for building app like chats and other types of application that require get events from server.