Introduction
Have you ever heard about SSE (Server-Sent-Events) before? It is a one-way messaging technology that works over the HTTP protocol and you can send messages from the server to the client. So, every time there is available data, the client will receive and update it in real-time.
How it works:
SSE has unidirectional communication, only the server sends events to the client, the events are captured by the EventSource API from JavaScript, which the browser uses to listen to events (we'll discuss this in detail later in the article). Using SSE, it's not possible to send binary data, only messages. We can find some SSE use cases in weather, sports, stock exchange applications, and so on.
Is SSE the only option?
There is more than one option instead of SSE, but neither is better or worse than the other, it depends on your needs. Besides SSE, you can find some alternatives like WebSocket and event data pooling, but if you only need to get some received data from the server, maybe SSE could be the best choice. Let me explain to you the differences between both:
WebSocket
A little bit different from SSE, the WebSocket is a protocol that has bidirectional communication between the client and server. Besides messages, it's also possible to send binaries in the payload. Some WebSocket use cases are chat applications and multiplayer games, which both require full-duplex communication.Event Data Pooling
Unlike SSE and WebSocket, the client should request data to the server, it usually takes more hardware resources because a lot of requests can be made until the server responds.
How is SSE implemented using Spring Boot?
Considering the article's focus is SSE, we will implement a practice example about how to use it in a Java Application, with the Spring Boot Framework.
We will use the Servlet Stack (MVC) because it's simple to understand and most developers are used to developing with imperative code. But, you can also implement it using the Reactive Stack (WebFlux).
Now, let's go straight to our use case.
Food Delivery
Let's try to understand how it works through a food delivery system. What we want to do is show the order progress to the user, the available statuses are: Order Placed, In the Kitchen, On the Way, and Delivered.
Considering it's only a prototype to illustrate an SSE use case, the system will take care of all order steps automatically.
Creating an SSE entry point
First of all, we need to declare an endpoint in our controller that returns a SseEmitter interface.
private final Collection<SseEmitter> emitters = new CopyOnWriteArrayList<>();
@GetMapping(path = "/order-status", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
SseEmitter orderStatus() {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
emitter.onError(throwable -> {
emitters.remove(emitter);
emitter.completeWithError(throwable);
});
emitters.add(emitter);
return emitter;
}
In the interface signature, we put the media type as a TEXT_EVENT_STREAM_VALUE to send real-time events to the client. Our method body is composed of a new SseEmitter added to a concurrent list to make broadcast sending possible. We also declare some events callback whenever a completion, error, or timeout happens.
Declaring who needs to know about the order
Once we have connected users in our endpoint, how can we determine that an order has arrived and needs to be prepared? Well, considering we want to send events to the client when the order changes status, why not apply an observable pattern and notify all listeners after each change?
Let's create a listener and register all observables representing each order status.
@Component
public class OrderFoodListener {
private final EventService eventService;
private final Collection<Observer> observers;
public OrderFoodListener(EventService eventService) {
this.eventService = eventService;
this.observers = List.of(
new OrderObservable(eventService),
new KitchenObservable(eventService),
new OnTheWayObservable(eventService),
new DeliveredObservable(eventService)
);
}
public void notifyAll(OrderFood orderFood) {
observers.forEach(foodObserver -> foodObserver.update(orderFood));
}
}
Let me explain to you what is happening here:
Event Service Interface: It's a dependency inversion interface of the way we go to send the events (concrete class should be declared in our infrastructure layer)
Observers Collection: All observers that we want to notify when an order comes.
Notify Method: The notifyAll method will be called to notify all observers about a new order.
Notifying the the customer's order
As we can see in our previous coding block, there are four
observables subscribed, each one with its own business rules.
Let me show you how we are going to notify the customer:
An order is coming
if (orderFood.getStatus() == ORDER_PLACED) {
sendEvent(orderFood, "order");
}
The order observable checks if the initial status is an order placed, if so it sends an event with the "order" event name.
It's going to the kitchen
if (orderFood.getStatus() == FoodStatus.ORDER_PLACED) {
orderFood.setStatus(FoodStatus.IN_THE_KITCHEN);
sendEvent(orderFood, "kitchen");
waitForProcess();
}
After our order is placed, it goes to the kitchen, so here we need to check if the previous status is still the order placed, then we change it and send a "kitchen" event with the updated status.
OBS. The waitForProcess() method is a thread sleep declaration to simulate a kitchen duration process
Now it's up to the delivery person
if (orderFood.getStatus() == IN_THE_KITCHEN) {
orderFood.setStatus(ON_THE_WAY);
sendEvent(orderFood, "on-the-way");
waitForProcess();
}
Before the order goes out, we need to check if it was in the kitchen, if so the user will be updated with the on-the-way status.
Finally, the order has arrived
if (orderFood.getStatus() == FoodStatus.ON_THE_WAY) {
orderFood.setStatus(FoodStatus.DELIVERED);
sendEvent(orderFood, "delivered");
}
Now it's time to celebrate and notify the user that it has arrived.
Receiving events on the client side
On the client side, the user will be waiting for order updates. So, let's implement it.
var eventSource = new EventSource("/order-status");
eventSource.addEventListener("order", (event) => renderEvent(event));
eventSource.addEventListener("kitchen", (event) => renderEvent(event));
eventSource.addEventListener("on-the-way", (event) => renderEvent(event));
eventSource.addEventListener("delivered", (event) => renderEvent(event));
}
We created a simple static HTML file and put some JavaScript to listen to all events from the server and print them on the screen.
Conclusion
I hope that in this article you were able to understand the main SSE concepts and know their differences between similar technologies. Additionally, we saw a practice example of how to implement it using the Spring Boot framework.
If you have any questions, please feel free to leave your comments below and I will try to help you.
I encourage you to check my GitHub repository and take a look at the whole code used in this article: https://github.com/GermanoSchneider/food-delivery-sse-app
Thank you, hope that I can see you next time.
References
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
https://www.youtube.com/watch?v=nxakp15CACY&t=2334s
https://www.youtube.com/watch?v=nxakp15CACY&t=2334s
Top comments (1)
Thanks for this post! It was actually helpful, I was able to send SSEs to a RP Pico on Hibernate PostInsert events~