When we design a system architecture, we should be extremely cautious about its complexity and how the system could be maintained in the future. Keeping in mind that maintenance is not only related to understand how the system works, providing help to end users, but mainly about adding new functionalities which could complement or modify previously designed behaviour.
Hence, we should focus on different approaches which could help us to achieve this goal. Being the main one how to tackle modularity and subsystems intercommunication. Even though, systems should be built in an iterative way, we cannot despise a proper architecture definition, which is obviously based on our knowledge and previous experience.
In this post I’m going to focus on one example that we have been recently working on, and how we have implemented a modular architecture approach, having in mind from the very beginning what we do wanted to achieve and, give or take, how we wanted to build it.
In this case we had to tackle the development of an internal management application, with some specific considerations, related mainly to how end-users where going to interact with it. Since, in fact, application was not only intended for internal users, but also it was targeted to direct end-users’ interaction.
Moreover, this interaction had to consider different input and output channels, e-mail, WhatsApp, Telegram, Web, or APP to name but a few. These requirements are quite diffuse, you could create a simple monolith which could fulfil, theoretically, expectations. But provided you analyse it deep inside; you could face some issues:
Thus, answering to all these questions requires designing the system to provide answers to all of them, which means:
Besides, a deeper analysis could give us another vision, not functional, but technical in which we could also provide answers to other questions as having independent development teams, as to end as soon as possible, but maintaining a common approach to the problem, avoiding repeating the wheel.
In the following scheme, I show you the full architectural vision of the system, a very schematic one indeed, but, perhaps, enough for having a better understanding.
As you can see, I’ve depicted different modules, or subsystems, which are intended for different tasks, providing you a glimpse of how we have achieved a global functional architecture and proper technical one.
I’ll try to give you a more detailed explanation in the following points.
The idea behind of this common API is providing different agents an input/ouput mechanism, which concern is also agent security.
As you can view this system is responsible of providing the input/output, but also is accountable of global system security on a per-agent basis.
Although it could seem pretty obvious, neither the web client nor the app can store security tokens to exchange information with the server, security considerations are very important here, never do that. That means that we have to create a middleware which is accountable not only of storing these tokens, but also of identifying users.
Besides whom has the responsibility of storing tokens, there are also some issues to have into consideration,
Do not reinvent the wheel. Design patterns are crucial when planning a proper architecture. Speaking about BOTS, you don’t have two different ones, you do have one common and shared subsystem and two – at the present moment – different implementations.
Moreover, as you can see in the previous picture, the involved systems try to generalize as most as possible,
As you can notice, I’m talking about typical software engineering patterns. Decoupling, isolation, single responsibility.
It’s difficult to imagine a system that has no need of events. Your system triggers events when something happens, and there are other systems (subsystems) which are subscribed to events to perform something. There is neither magic involved nor you need a complex Kafka queue to support events. It’s just a simple producer-subscriber pattern.
Despite simplicity, if you want to create a truly modular system you need to add support for some "complex" problems as:
Hence, when you have to develop a system you should probably consider that you are going to need events and that doesn’t mean that you need a Kafka Queue, those are implementations details… a proper architecture should allow to replace your custom implementation for an alternative one if that’s needed in the future.
As I’ve stated in previous posts, perhaps one of the main problem when you develop a complex application is that a typical development workflow is not enough. You cannot end up with a monolith in the frontend, that is complex to maintain, complex to develop and complex to test. My proposal, divide your frontend in different micro-frontends, each of them should have its own micro-backend layer, as shown in the following pictures.
As I stated when I talked about subscribers’ discovery, supporting microfronteds requires, again, hot discovery, custom classloaders, and a pretty complex configuration for bundling micro-backed and micro-frontend in a single deployable file. Fortunately you have to create the configuration only once, applying a proper Gradle multi-project configuration.
Besides the file bundling, there are some other matters to consider,
You don’t need to create utterly complex architectures, use only what you need. But keep in mind that you don’t have to seek simplicity for the sake of it. Some things are complex by nature, and your mission as an engineer is to make the complexity operative.