Where to put WebSockets?

-

In a typical 3-tier architecture of your application, you have three tiers or layers called presentation, application, and data. In a Spring Boot application, these can be implemented as controllers, services, and repositories. In this architecture, the controllers might form an API of your application to which the front-end communicates. Now where to put the WebSockets? And more specifically from where to call them?

It depends of course what the required functionality is. In our case, we wanted other user’s updates (can also be creations or deletions) to the database to be pushed to the rest of the users. What would be a ‘logical’ place for this?

We put the WebSockets themselves into the presentation layer. This means that there is a (Spring) component that sends the actual messages via the opened WebSockets to the front-end (UI). This is a logical place for sending and receiving messages.

An intermediate code sample in a Spring context:

@Controller
@Slf4j
public class WebSocketController {

    private final SimpMessagingTemplate messagingTemplate;
...
    public void sendCreated(DomainFullDto domainObject) {
        log.debug("Sending created domain object via web socket: {}", domainObject);
        messagingTemplate.convertAndSend("/topic/domainobjects", new WebSocketMessage("DOMAIN_OBJECT_CREATED", domainObject));
    }
}

But then the more tricky part. Where to call them from? What is the trigger?

At first, we put calling the WebSockets in the controller. If a user sends an update of a domain object, the receiving endpoint not only sends the updated object back but sends this object also to the other/all users through a WebSocket. This seemed logical because all code for the communication to the front-end remained in the presentation layer.

Later on, we build other functionality which also updates domain objects triggered by scheduled jobs. Because no user interaction triggered these updates, we could not place any hook in the existing code of the controllers. We did want the updated domain objects to be immediately available in the front-end. So the triggering of the WebSockets needed to be placed elsewhere.

I didn’t want the triggering to happen from all over the code, so I proposed a new centralised place. I used aspect-oriented programming (AOP) to build this. This is a cross-cutting concern to place hooks to trigger some other code. in our case the trigger should be ‘successful modifications to the database’. So I put aspects on the successful returns of methods of the repository for database manipulations.

3-Tier Architecture with WebSocket Aspects

The aspect is responsible for catching the updated domain object(s), transforming it to data transfer object(s) (DTO(s)) for the front-end and sending it through the WebSockets.

A code sample in a Spring context:

@AfterReturning(pointcut = "execution(* eu.luminis.application.datastore.DomainObjectRepository.save(*))", 
            returning = "domainObject")
    public void afterSaveDomainObject(DomainObject domainObject) {
        log.debug("Caught saved domain object to send via websocket: {}", domainObject);
        var savedDomainObjectDto = domainObjectMapper.domainObjectToDto(domainObject);
        webSocketController.onSaved(savedDomainObjectDto);
    }

Now if future development of the application also requires manipulation of the database, automatically the functionality is used to send the updates to the front-end. No need for developers to think about it anymore. It is future proof!

Other things to consider

  • What are the disadvantages? A disadvantage of this setup is that all successful updates of the domain objects are always sent via the WebSockets. If you do not want certain updates to be pushed, you have to create alternative flows for them or make the setup less generic. This could make things more confusing.
  • Do you send the updated domain object DTO double to the user who did the update? That is as a response to a REST update request and through the WebSocket? Due to for example a firewall some users might not have the ability to receive WebSocket messages. For them, it is important that they still receive the REST responses.
  • How to handle the updated domain objects in the front-end? Especially if you receive them double? To prevent sending them double you could do a check in the back-end before sending a message through the WebSocket. For this, you need to know who triggered the update. It might be easier to just send messages to all users. In the front-end, you just process updates received from WebSockets differently than from REST responses.

Feedback

If you have any questions or remarks, please let me know!