The 4 shades of UI composition
In larger projects, the cooperation of several teams and long-term maintenance is always an issue. The architecture must be defined in such a way that such aspects are possible. This is often implemented with a certain degree of modularization.
This applies not only to the backend, but also to the frontend. For a long time, frontend applications were somewhat neglected. Often the frontend was developed by the designer and cross-directional beginners or was done by the developers on the side. After all, pixel pushing was not so popular with the developers. The "real" work, namely in the backend, was given more attention and done seriously. This inevitably led to the frontend being more or less a patchwork. But user interfaces in general have become very important in recent times. Hardly any application can be brought to the people these days without an appealing user interface. And that makes it all the more important that the above-mentioned attributes are also taken into account in the frontend.
I have worked on several, very large, projects in my career. I have seen and helped develop several types of modularization in the frontend. The community has also moved forward. Topics such as microservice architecture has also an impact on the frontend architecture with new practices and patterns.
In this article I am going to discuss some of these modularization patterns.
I would like to start with traditional monolithic architecture. What used to be the standard is somewhat unpopular today, but it has its justification.
Especially for smaller software projects, it is still common to fall back onto this architecture. Using a certain architecture is always associated with compromises. In case of smaller projects, this is definitely in favor of a monolith. The advantages are clearly in the simplicity of a monolithic architecture. There is only one system that needs to be developed and maintained. And the communication between the components, apart from an SPA frontend, takes place exclusively in memory.
But this does not mean that a monolith is completely un-modularized. There are many software architecture patterns such as layered architecture, onion architecture, ports and adapters or the popular Clean Architecture pattern from Uncle Bob helping structuring a software application.
However, these have in common that they modularize an application in horizontal layers. This horizontal orientation of, not only of the presentation layer, but of the whole application makes certain problems more complicated. I am talking about issues such as working in several teams, the independent deployment of each component and also long-term maintenance, including the transition to new technologies.
At some point the compromises become too painful and it is worth moving on. But at what point, or at what size of software application is this point? Personally, I always use the single responsibility principle as a guideline. I am not talking about all the infrastructure plumbing that is always involved. But as soon as an application has several responsibilities in terms of business features, it is time to consider an expansion of the architecture.
To mitigate these problems, the application must be modularized vertically. This means that the application is divided into modules across all horizontal layers. A module in each layer has its own, more or less independent, part. From the frontend down to the database.
The boundaries of a module are usually defined by a business feature or a group of features. For this purpose, the methodologies of Domain Driven Design are often utilized and the application is subdivided into so-called bounded contexts. Then a module is generated for each bounded context.
Modularization also raises the question of how far to go. We have a wide range of options, from simple folder structures to distributed systems. But as soon as communication goes beyond the process boundary, the complexity of the system increases considerably. Because then, topics such as API versioning, service availability, service discovery, network instabilities, authentication and authorization have to be dealt with.
Again, the trade-offs of the available options need to be weighed. You want to make your application better structured, more maintainable and more accessible for several teams. However, people often fear the costs of a distributed system. This brings us to the compromise where simple approaches such as folder structures are used to split an application into modules within the application boundaries. We are talking here about modularized monoliths.
As mentioned above, there are many technical possibilities to modularize an application within the application boundaries. The simplest way is to organize the code into a specific folder structure. In some projects, they even go a step further. The code can also be organized in its own libraries (jar, dll, npm etc.). This works in the backend as well as in the frontend. And with a little extra effort, an independent deployment can even be achieved via drop in replacement.
But a modularized monolith also has its limitations. First of all, it is difficult to enforce the design and module boundaries. Violations creep in very quickly. This has a lot to do with the discipline of the developers. So you have to be careful not to accumulate a lot of technical debt over time. On the other hand, the modules are highly interdependent. Technology selection, deployments and maintenance cannot be chosen independently at all, or only to a very limited extent. Even in the frontend, a decision is made for a technology that is used for the entire application. Upgrades or even a migration to newer technologies are difficult and can be very expensive.
I have seen many modularized monoliths in my career. 10-20 years ago this was simply the standard architecture used for web applications. There was early talk about service-oriented architecture (SOA). Of course, this alleviates the problem, but only in the backend. The user interface is still mostly a monolith.
We are often asked to revitalize such systems, i.e. modernize them. In most of the cases we fall back on a more modern architecture that is related to micro services and micro frontends - the Self-Contained System (SCS). Read my article about application revitalization.
There is a lot going on in the development community about microservices. Microservices solve exactly the problems I mentioned earlier and bring even more advantages, such as independent scaling. But also with microservices we tend to have a UI monolith. The community has an answer ready for that as well, micro frontends. Before we get into this topic, I would like to talk about self-contained systems, a special kind of micro frontend architecture.
I have already mentioned that you can cut a monolithic system along its domains and wrap it into modules. Taking the approach further and wrapping each domain into separate, replaceable web applications, we refer to this application as a Self-Contained System (SCS).
An SCS contains its own user interface, specific business logic and separate data storage. They communicate with other system via hyper links, RESTful services or asynchronous messaging. An SCS is responsible for its core domain and master of its own data. Data and logic can be shared via a well defined API. An SCS can consume microservices to solve domain specific problems.
You see, this way every SCS can be developed with its own platform, frameworks and release cycles. This enables a future proof and maintainable system. Of course this comes with a price. Every application needs to have its own CI/CD pipeline, its own deployment procedure. And we need to think about how certain constraints, such as common layout, styles and components can be guaranteed.
This problem can be solved with different approaches as well. A pragmatic solution would be to do nothing at all and leave design and layout to the SCS. In certain cases, this may even be desirable.
A step further is the introduction of UI guidelines, which are not enforced, but provide a strict framework. This still gives the applications a lot of freedom and maximum independence. Each system must ensure itself that it adheres to the guidelines. Accordingly, different interpretations and adaptation speeds occur quickly. Under certain circumstances, the system may not appear to be made from a single mold.
If you want to enforce a design, you cannot avoid to shared something. However, there is always the risk that the individual SCSs will experience a technology lock. For example, if you make a library with Angular components, the SCSs are forced to implement their frontend in Angular. And that is exactly what we want to avoid. I have had good experiences with extracting style sheets with colors, spacing, typography, etc. into a library and integrating them into the applications. It is also a good idea to make a technology agnostic component library with WebComponent. There are frameworks like StencilJS that are specialized for such use cases.
What has also worked well for me is using the microfrontend approach for layout and components like headers and footers. This is done by using web components that are compiled into a single file bundle and hosted in a CDN. An SCS can dynamically include these components at runtime via
<script> tag. By the way, this also works very well when an SCS needs to render concrete content of another SCS. We name it widgets. This ensure to keep the responsibilities of the SCS focused on its own domain. I wrote an article about this topic a while ago.
This way we achieve maximum independence for shared components. But still, don`t forget that everything that is shared hurts and you want to avoid this!
The last UI composition pattern I want to cover is the microfrontend architecture pattern.
This pattern has its origin in the microservice architecture. Microservices solve many of the problems already discussed. But with microservices we still end up with a monolithic UI.
This means that the advantages we gain from the microservice architecture are lost in the frontend. But we actually want independent teams, technology selection, deployment, scaling and release cycles in the frontend as well. Microfrontends address this problem.
The idea is that each microservice also has its own frontend. Compared to a self-contained system, however, this is not an independent application. In a microfrontend architecture, a so-called shell is needed. The shell is a very lean web application that combines the frontends of the individual microservices into one application. The communication of the microservice is preferably done via microfrontend. That means, via hyperlinks or HTML properties and events.
An example could be a shop selling video games. Team product is responsible for the product page and everything which needs to be included here. Team checkout is reponsible for everything regarding the pruchase process and team marketing manages the product recommendations on this page.
Again, there are many technical ways to implement this, some of them are:
- The micro frontends can be integrated via iFrames
- Compose your frontend with WebComponents
We also have the same problem with sharing styles and layout across the micro frontends. We can rely on guidelines or develop a framework agnostic UI library with web components.
I covered the 4 most used UI composition pattern I am aware of. Its starts with a monolithic system, breaking it into modules and Self-Contained Systems. At the end of the line I covered the microfrontend architecture.
One of the key points here is considering trade-offs. Mostly between simplicity and the advantage of independent teams, technology stacks, deployments and release cycles.
And one thing to mention again: it hurts to share code!
What do you think about those UI composition patterns? What is your experience? Leave a comment and discuss it with me.