Architecture Design
Following the identification and definition of the quality attributes, a microservice architecture was chosen. This is characterized by the decomposition of the system into a set of independent and loosely coupled services, each of which is responsible for a specific business domain or functionality. Indeed, the use of this architectural style enables the continuous delivery and deployment of large, complex applications, the ability to scale horizontally with ease, the possibility to use different technologies and programming languages for each service and an increase in the overall system maintainability. Moreover it allows to isolate failures and to improve the fault tolerance of the system, hence gaining in reliability.
Microservices Decomposition
Following the decompose by subdomain strategy the following microservices have been identified:
- User Service: responsible for managing the user account data and the groups of the system;
- Location Service: responsible for managing the location and the user tracking;
- Notification Service: responsible for managing the notifications;
- Chat Service: responsible for managing the chat messages.
Both the Groups and User bounded contexts have been aggregated within the user service due to their strong interrelation and need for frequent, seamless interaction. Separating them into distinct microservices would compromise data consistency and introduce unacceptable levels of latency; merging together the bounded contexts make the integration more efficient and reliable. Indeed, while it’s common to map a bounded context to a single microservice, this isn’t always the case 1.
Moreover, to aggregate the functionalities of the different microservices, we have chosen to use the API Gateway pattern. This pattern is used to aggregate the functionalities of the architecture, providing a single entry point for the client applications. The API Gateway is responsible for routing the requests to the appropriate service, aggregating the responses, and providing a unified interface to the client applications.
Architecture Documentation
High-Level Overview
To address the challenges of scalability, decoupling, and real-time processing, the system architecture was designed following an event-driven microservice approach, in which the microservices are mainly designed to interact by means of event streams.
This paradigm enables asynchronous communication between the services, allowing them to be loosely coupled and independent from each other, hence making the system more resilient and maintainable.
For this purpose, the architecture is designed around a Message/Event Broker, which acts as a central communication hub for the microservices, enabling them to publish and subscribe to events, and exchange messages in a reliable and scalable way.
Nonetheless, regarding the communications between the client and the microservices through the API Gateway, an RPC protocol is used, in order to ensure synchronous communication. This approach enables the client to invoke remote procedures on the microservices and receive a response in a synchronous manner (e.g. for the authentication process).
Lastly, each microservice follow the best practice of having its own database, ensuring data isolation and independence from other services, allowing a loosely coupled architecture whose communications happen only through the message broker via a standard protocol. This has also the advantage of letting the developers change a service schema without affecting, and thus coordinating, with other services teams.
Following these high-level principles, in the following sections we provide a detailed view of the system architecture, though the three main structural views: Components and Connectors, Modules and Allocation.
C&C View
The following diagrams shows the Component and Connector (C&C) view of the system, providing a high-level picture of the system’s runtime entities in action and their boundaries.
In order to avoid overwhelming the reader with an all-encompassing but rather confusing scheme, we provide below a C&C view of the system by showing, for each microservice, its relative UML diagram.
User and Group Service
The user service interacts with the Gateway, exposing an RPC-based API port allowing users to manage their account and groups. It has its own database for storing user and group data and interacts with the message broker to publish group events to the other services.
Location Service
The Location Service interacts with the Gateway, exposing two different API ports: one for real-time tracking via a real-time connector, allowing users to be tracked, and another for accessing tracking service information through an RPC connector. Like any other microservice, it has its own database for storing the tracking data, and it interacts with the message broker to publish notifications and subscribe to group events.
Chat Service
Similarly to the location service, the chat service exposes two different API ports: one for real-time chat communication via a real-time connector, and another for accessing chat service information through an RPC connector. It stores the chat data in its own database and interacts with the message broker for receiving group events.
Notification service
The notification service expose a input port through which an RPC connector is possible to subscribe to notifications by clients. It has a data access port to interact with the database and two output ports to receive group updates and notification commands.
Deployment View
Hexagonal Architecture
In compliance with DDD principles, the microservices are designed following the Hexagonal Architecture pattern (also known as Ports and Adapters or Onion Architecture), which is a particular instantiation of the layered architecture, that is well-suited for microservices to preserve models’ integrity. Indeed, the primary advantage of this pattern is the separation of concerns, which allows the business logic to be decoupled from the infrastructure and the external systems (e.g., databases, message brokers, etc.) hence making the system more maintainable and testable, and the business logic more reusable.
Each layer of the Hexagonal Architecture has been enforced in the code by mapping them into modules (like Grade submodules) each of which with its own build dependencies and responsibilities, as shown in the following diagram: