The 4 shades of UI composition

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, front-end 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 front end 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 have 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.

The monolith

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 the 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 a 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 structure a software application.

Clean Architecture|360x240

However, these have in common that they modularize an application in horizontal layers. This horizontal orientation of, not only 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 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.

Modularized monolith

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.

Modularized Monolith|434 x 291

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.

However, 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 cases, we fall back on a more modern architecture that is related to microservices and micro frontends - the Self-Contained System (SCS). Read my article about application revitalization.

Self-contained systems

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).

Self-Contained System|387x290

An SCS contains its own user interface, specific business logic, and separate data storage. They communicate with other systems 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 and its own deployment procedure. We need to think about how certain constraints, such as common layouts, 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 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 sharing 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 ensures that the responsibilities of the SCS focused on its own domain. I wrote an article about this topic a while ago.

Shared layout in SCS|387x244

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!

Micro frontend

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.

UI monolith with microservices|431x297

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.

Micro frontends|431x371

An example could be a shop selling video games. Team product is responsible for the product page and everything that needs to be included here. Team checkout is responsible for everything regarding the purchase process and team marketing manages the product recommendations on this page.

Micro frontends example|588x366

Again, there are many technical ways to implement this, some of them are:

  • Manually load and embed an HTML file via JavaScript

  • The micro frontends can be integrated via iFrames

  • Compose your frontend with WebComponents

As you see, the technical support for this pattern isn't that great. There is still a lot of manual work involved. Nobody wants to manually load and embed HTML via JavaScript and iFrames are troubleson concerning scaling of its content. The most promising approach is to compose your frontend with modern web components. There are a lot of frameworks that are specialized in authoring web components such as Angular Elements, StencilJS, or LitComponent. This works the same way I used to integrate a header into a Self-Contained System. Web Components get compiled to a single file bundle, deployed to a CDN, and consumed via <script> tag.

We also have the same problem with sharing styles and layouts across the micro frontends. We can rely on guidelines or develop a framework-agnostic UI library with web components.

I want to mention at this point, that there are also JavaScript frameworks emerging which are specialized in microfrontends. One of those is Single SPA. I never tried it out so far, but it could be a big help in doing microfrontends.

Summary

I covered the 4 most used UI composition patterns I am aware of. It 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.