
Transformation Towards Paperless Banking Solution
SignalR scale out with RabbitMQ
One of the last projects I worked on as part of Banca Intesa’s team, and also one of the most interesting in my 26-year-long IT career, required real-time communication between backend and frontend. We all know that real-time communication can be process/time critical, but this time that wasn’t the case. Let me walk you through the process we went through to find the best solution while implementing a new functionality within our PowerWeb application.
Task at hand
As part of a bigger Paperless business development project, we had a task to implement an option that will enable clients to sign documents using a digital certificate along with the requirement to show live updates of the signing process to the consultant in the PowerWeb application, and to the client on an additional screen.
Having in mind that the PowerWeb application was developed using Angular/TypeScript with WebAPI service as the backend, developed using .NET 8/C#, and based on research, we decided to use WebSocket protocol for all communication between backend and frontend. Technologies used guided us towards Microsoft solutions.
WebSocket
WebSocket is a communication protocol for web applications, that allows independent and simultaneous bidirectional communication between client application and server.
For the web application to be able to communicate using WebSocket protocol it must use WebSocket API, and it is available in all major web browsers.
This is beneficial, but we had to be aware of a number of messages that will be received by the client, as the WebSocket API doesn’t support backpressure. In the situation that the client application cannot process all messages received, it will become unresponsive. Therefore, we designed our solution in such a way as to limit the number of messages exchanged between the client application and server.
Also, when there is a need to develop complex solutions, such as ours, it can be time-consuming to use WebSocket API directly. A better option would be to use any of the available libraries, such as SignalR, as it incorporates libraries available for both .NET/C# and Angular/TypeScript and supports WebSocket as one of the protocols as well. And that is actually what we did.
SignalR
Just to introduce, SignalR is a real-time web communication framework developed by Microsoft, designed to enable asynchronous, two-way communication between server and client over the web. It allows web applications to push content to connected clients instantly, without the need for the client to repeatedly request updates. In web applications, SignalR can be used for features that require instant updates, such as live chat systems, real-time notifications, collaborative tools, and live dashboards. It enhances user experience by providing real-time interactivity, reducing the need for constant polling, and ensuring up-to-date information delivery.
Sending and Receiving data
SignalR uses message-based transport, so all data is sent and received using messages. Each message must be encoded before it is sent, and there are two types of message encodings:
- JSON and
- MessagePack.
Deciding what encoding to use depends on the needs of the application. JSON is human-readable, native to JavaScript clients, but the payload is bigger compared to a MessagePack. If the speed of data transfer is a key requirement and the amount of data sent is on the bigger side or it can’t be encoded in JSON, MessagePack is better. We ended up using JSON, as the amount of data exchanged was small.
Messages to a client can be sent in multiple ways to all clients connected to the SignalR Hub, to a specific user, or to a group and to be able to send messages to a specific user, authorization must be enabled on the SignalR Hub. On the other side, in order to receive messages sent to a group, the client must subscribe to the specific group.
SignalR on a multi-server environment
There is no need to point out that big systems, such as ours, have a complex infrastructure that includes a multi-server environment for system resilience and availability. This means that we had to find a way to implement SignalR in a multi-server environment.
The issue
The SignalR client can be connected to a single SignalR Hub. This can create issues if there are multiple servers involved (ex. Server farms). If, for example, one server wants to send a message to all clients, the message will be delivered only to the clients connected to that specific server. This is the exact situation that we faced as our environment consists of microservices hosted on a server farm.
The solution
One of the solutions could be for the application to create multiple instances of SignalR client and to connect each instance to a different SignalR Hub. This can be hard to implement, if servers are behind load balancer or in containers (ex. Kubernetes). And let’s be honest, this is not an optimal solution by any means.
Better would be to use an additional service that can distribute messages to SignalR Hubs on all servers and the framework for that is included in SignalR - Backplane.
One of the possible solutions was Azure SignalR Service, too. Azure SignalR Service is a proxy and backplane that enables scaling out SignalR across multiple servers. The downside is that this solution requires a connection to Azure, which is not always possible.
At last, there was a solution to use a messaging broker to distribute messages between different instances of the service.
We have already been using RabbitMQ as part of the system optimization for resilience and availability, so we had an easy choice.
RabbitMQ
RabbitMQ is an open-source message broker widely used in microservice architectures to facilitate communication between loosely coupled services. In a microservice environment, such as ours, where each service operates independently and can have its own lifecycle, RabbitMQ helps coordinate and manage the flow of messages between services without creating direct dependencies. By enabling asynchronous message passing, RabbitMQ decouples microservices, allowing them to communicate more efficiently and scale independently. RabbitMQ ensures reliable message delivery and fault tolerance, making it an essential component for building resilient and scalable microservice systems.
It can be used in various scenarios, such as Asynchronous Task Processing, Microservices Communication, Event-Driven Architecture, Scaling Applications, and Load Balancing.
RabbitMQ's ability to handle complex messaging patterns, coupled with its reliability and scalability, makes it an essential tool for building modern, distributed applications.
Implementation
But to be able to use RabbitMQ as the backplane for the SignalR we had to implement abstract class HubLifetimeManager, which handles all internals of its communication. This includes sending and receiving messages.
For relaying messages, RabbitMQ uses the concept of Exchange, queue and routing keys, so it was required from us to implement all functionalities of Hub Lifetime Manager. Therefore, as each SignalR hub uses a single instance of Hub Lifetime Manager we are now able to relay all communication using a single exchange. As every exchange must have a name, it is better to use the name of the SignalR Hub class. We have selected the topic as an exchange type so now we can send the same message to all servers using the same set of routing keys. For topic exchange type messages are routed to all queues that have a binding key that matches the routing key.
Based on that, each server now creates a set of queues and binds them to exchange using a predefined set or binding keys. As queues must have a name, for every queue a unique identifier is generated and is used as the queue name.
Sending messages
To be able to better explain this part of implementation I will assume that there is only one server. On the other side, when multiple servers are involved each server has its own set of queues and they are using a predefined set of binding/routing keys.
PowerWeb application is used by consultants, and that is why we decided to send messages to the specific (authenticated) user. To be able to send messages to all connections a queue/routing key par was created for each user. Messages received through this queue are sent to all connections on that server that are associated with the user.
20 years a step ahead of time
As an IT department of the domestic leading bank which is part of the International Intesa Sanpaolo group, we have best practices and coding standards. Thus we use the internal framework that allows us to reuse well-tested and optimized functionalities and focus on solving business requirements.
As real-time communication between frontend and backend may be used in a number of ways, we have added (SignalR RabbitMQ Backplane library) into our internal framework, making real-time communication in any of our current or new projects easy to use.
Author: Igor Čolović, Software architect for multi-channel and CRM application solutions