In our experience, from a functional point of view the use of frontends can be split into three general business scenarios:
In recent years the microservices architecture style has taken root in many projects. The overriding idea here is to break the entire system down into smaller, easily serviceable units with the aim of achieving close functional cohesion within the respective microservices and the least possible coupling with each other. This allows components of the overall system to be enhanced independently of each other. It should be noted that the functional breakdown itself is also the critical part, as this decides whether the goal of small, independent units will be achieved or whether it has only resulted in the implementation of a spaghetti code monolith using distributed services. When implemented correctly, microservices promise the advantages outlined in Table 2.
Especially for services that are primarily connected to each other via APIs or which provide them as public APIs, the advantages appear to be very desirable. But does this still hold true if an interface has to be provided for users whose use case requires the interaction of several microservices? Can they still enjoy the advantages of microservices in this case? Or does the expansion bring any significant disadvantages? To answer these questions, your first step is to identify which architectural approach you have chosen for the frontend. In addition to the functional driven view at the beginning, we are talking here about the technical view of micro-frontends.
Basically, there are two approaches: frontend monolith, and frontend integration.
The frontend monolith spans all microservices with a central frontend. With frontend integration, however, each microservice is responsible for its own frontend – and the challenge is to integrate these with each other in a meaningful way for the user. In recent years the term ‘micro-frontend’ has also established itself as meaning ‘frontend integration’. ThoughtWorks used the term for the first time in 2016 by in their Technology Radar (read more).
Both approaches have their raison d'être for certain use cases as well as their associated strengths and weaknesses. We compare the different approaches by reviewing whether the advantages of microservices mentioned above can be maintained. In addition, we share insights from our experience as to whether any possible resulting disadvantages also have a negative impact within the respective business scenario.
If all microservices provide APIs for their business functions, it seems obvious to address them via a central frontend component and thus make them uniformly accessible to users.
In this scenario, the frontend is developed as an independent unit and communicates with the various services via defined APIs. Backend teams only provide features via (versioned) APIs, and a dedicated team creates the frontend for all other teams (see Figure 1). This creates a horizontal frontend layer on which all other teams depend. In terms of a microservice architecture, the teams are no longer independent. Features are only made available to the user as soon as a new frontend release has been delivered.
A gateway layer can simplify API access. In addition, the gateway layer can also perform authentication or caching. If you plan to use several frontend monoliths (native app, web), you can connect these via several specialised gateways (read more). Nevertheless, you should always set clear rules to ensure maintainability and prevent parts of the business logic from diffusing into the gateway, in addition to the data transformation.
Through the uniform implementation of UI and UX, both within the vertical microservices and in overarching use cases spanning multiple microservices, the application acts like a classic monolith for end-users. Table 3 shows what impact the frontend monolith has on the advantages of microservices.
If I only have a small number of teams or little functionality to cover, a frontend monolith, or layer, can be a good solution; whereas if I have a larger number of teams or a very broadly-based functionality, i.e. domains in the sense of a Domain Driven Design (DDD), those are the first tell-tale signs that a micro-frontend architecture should be considered. In doing so, one of the frontend-integration patterns described in the following sections should be checked to see whether it is applicable.
If, at the time of the architecture decision, it is unclear how the domain will be subdivided, or how many teams are to be formed, the first step is to choose a simple solution that neither pre-empts too many decisions nor results in a major ramp-up effort. Therefore, there is nothing to prevent you from opting for a frontend layer first – although that does not free you from the need to design and develop this layer very neatly. Clear domains should also be found in the frontend layer, and the domain and technology should be separated from each other.
If, at a later stage, there are reasons to develop the architecture into a micro-frontend, it will take considerably less effort to do so. With regard to the business scenarios, there is no clear preference for the frontend monolith. However, for the B2C area you should check the requirements in terms of development speed (e.g. through independent deployments) and whether the necessary scalability of the application can be achieved. In particular, assess whether individual business components in the frontend could be more frequented and thus have to remain separately scalable.
Frontend integration is where the frontend is part of the respective microservice and is jointly developed by its team (see Figure 2). Several options exist for integration with other microservices. In the simplest case, the functional contexts of the microservices are so clear cut that integration is easy to achieve using a commonly used navigation structure. HTTP links between the contexts make it easier for the user to jump between different contexts, if necessary. This variant quickly reaches its limits as soon as content from different contexts always has to be presented to the user in parallel. Advanced integration can be achieved in different ways: build-time integration, integration on the server, or integration on the client.
Build-time integration is based on providing the frontends using packages, which a container app then loads as a dependency. However, this means that if there is a change to a micro-frontend, I have to rebuild and deploy everything.
As such, you should avoid this approach at all costs. Whenever a UI element is changed, all product elements must be rebuilt. On a code basis, the services are well separated from each other so they can be developed and tested independently. With build-time integration, however, the services are closely coupled once again. It’s therefore essential to choose an architecture that integrates the frontend only at runtime. Webpack5 and Module Federation offer an exciting approach in this regard, where the dependencies only need to be defined at build time. The individual modules are delivered independently in separate deployment units. More about this can be found here. This approach is independent of the UI framework and works robustly at least with Angular and React, even if Webpack5 is only available as a beta or RC2 release. There are two advantages to this solution:
Firstly, you do not have to deal with all the disadvantages of build-time integration, and secondly, the dependencies are validated. However, all the teams need to use Webpack5.
Runtime integration on the server
One of the simplest solutions is integration on the server. Frontends, which consist of several components of different backend microservices, are resolved by the server and bundled to the client. The client then only needs to make one request (see Figure 3). The strength of this comes to the fore particularly in an environment in which higher latency times are to be expected or connections are not always stable. The technologies for implementation are available at the web server or cache level with Server Side Includes (or Edge Side Includes). Implementation using separate frameworks (e.g. Tailor) is also possible.
Server Side Includes (SSI)
SSI is an established technology that has its origins in the 1990s and is still supported by common web servers (e.g. nginx, Apache, IIS). It does not matter whether you opt for SSI or ESI when considering the architecture pattern, so we will use a solution involving SSI for further consideration.
If further include instructions are contained in the page requested by the client, these are resolved by the web server or cache and the referenced page is already included in the response. A major disadvantage is that the slowest service determines the response time. The entire system can be designed to be resilient so that I can respond quickly to a failure. However, the web server or proxy is the single point of failure here and should therefore be kept lean and redundant. Cross-sectional functions, such as authorisation or authentication, can also be achieved in the proxy.
This variant has clear advantages for scenarios in which it is important to minimise the time until the end-user sees something in the browser, but only if all services behind the proxy respond quickly. For this reason, this variant is, in our opinion, worth considering, especially for B2C and B2B business scenarios where response times are critical.
Runtime integration on the client
Single-page application (SPA)
A SPA is a web application that consists of a single HTML page. All necessary resources and libraries are delivered the first time they are accessed. Changeable content is loaded dynamically. This content is processed on the client in the browser and the display is updated.
These allow you to build reusable components based on Web Component standards. They are reusable HTML components that work across browsers. Web Components were published as a standard by the W3C way back in 2012 and are now supported by all common browsers. Essentially, Web Components comprise Custom Elements, the Shadow DOM, and HTML templates. The advantage of Web Components is that they can be implemented independently of the libraries and frameworks used (such as Angular or jQuery). The interfaces to other teams then form UI components, i.e. the composition of presentation, behaviour and data, and not just an API and data objects. Provision of and access to the implementation are required for the integration. The Web Component itself handles communication to the backend.
Based on the business scenario, there are no clear advantages or disadvantages for B2B, B2C or in-house scenarios. The only thing you should keep an eye on in B2B and B2C target scenarios is the increased number of server roundtrips.
In addition to the frontend variant used, in the following we summarise the further experiences we have gained.
Integration of micro-frontends into the existing IT ecosystem
Whenever a new system is created, the context always involves legacy systems. Either a replacement for an existing system is to be created or the new functionality is required.
The pattern for integrating legacy systems into a micro-frontend ecosystem often depends on the age of the applications to be integrated, or the architecture and technology stack.
There is no need to completely redevelop all frontend functionalities on the legacy systems, as even providing smaller functional contexts is often enough to generate business value overall. Two possible scenarios for integration and use are successive replacement and adaptation.
The gradual replacement of the legacy systems is governed by the business value that a redesign brings with it. This requires parallel development and parallel operation in both systems. For the replacement, the legacy system is temporarily integrated into the new system – for example by creating an API facade pattern. This facade hides the legacy system while integrating the new system’s services. Functions from the legacy system are used and integrated into the new microservice and the micro-frontend architecture via the facade. Little by little, fewer and fewer functions of the legacy system are used. It is unclear to the user whether they are working with the new systems or with the legacy system. This pattern is known as the ‘strangler’ pattern. To find out more about the replacement scenario, read about it here or here: C. Richardson, Microservice Patterns, Manning Publications, 2018.
Adaptation is about increasing the scope of functions of the legacy systems. These adaptations can be technical extensions, for example in that access via native apps on mobile devices is required – or new functionality is required and this must interact with legacy systems. Specialised API layers can be used here as a pattern to avoid general-purpose, extensive API layers. These specialised layers are known as ‘backend for frontend’ layers. They are also suitable if the legacy systems are to be integrated into the new micro-frontend ecosystem. The legacy system can then be used in parallel via two frontends. More on the subject of backends for frontends can be found here or here: S. Newmann, Monolith to Microservices, O’Reilly, 2020.
Every microservice architecture has repeating requirements that every team must meet, and the same applies to micro-frontends. These should not be solved completely independently but coordinated between the teams. In our experience, the following functionalities must be considered and agreed in the overall context (see Figure 5):
We prefer to provide patterns that are developed once and which can then be used by other teams. No single team is then responsible for these functionalities. It is best to consider patterns as a template: each team can expand the scope, improve the architecture pattern, or eliminate errors as they wish. The other teams can then use the customised patterns.
Maintenance by a central team must be carefully considered and evaluated, as this results in all the teams becoming horizontally dependent – something that should be avoided in the terms of the goals of microservices.
You should agree a common pattern library (see the example of OTTO here) to establish a common look & feel. Central provision allows the microservice teams to achieve results faster. The task is to find a way in which this can be applied. Centralised management and acceptance are not very helpful; the teams should be able to set up the frontend themselves using the pattern library in accordance with defined frameworks. It may be appropriate to establish a group of usability and UX experts: this team can take care of topics such as user surveys, comparative studies, usability analyses and usability tests, as well as holistic considerations with the help of user journey maps. However, this scaling is also a matter of the size of the overall system.
Patterns are created in addition to achieving the goals derived from the non-functional requirements on their technological frameworks. In our experience, these have proven to be helpful and conducive to achieving the aforementioned goals:
Due to its simplicity and robustness, server-side implementation based on Server Side Includes is very often a good solution, as there are few restrictions and requirements on all teams. However, it is important to keep the SSI integration layer as lean as possible and only implement the key cross-cutting concerns in one place. If the integration layer becomes too dominant, this gives rise to the risk of technical dependencies and a significant increase in the danger of costly breaking changes. While it remains to be seen whether Webpack5 will establish itself and offer a robust solution, the current release looks promising. Web Components are supported by more and more browsers and are specified by a standard. On the whole, Web Components are a good solution if UI components are suitable as an abstraction layer in the context of individual requirements.
If the business domain is manageable in scope and not very broad, this can be a sign that I don't need a micro-frontend.It may be sufficient to design a good microservice architecture for the functionality and use an overarching fronted layer at the frontend. However, in this scenario there is a risk that the implementation of a micro-frontend will create too much overhead and unnecessary complexity.
If many teams develop the product and the domain is highly fragmented, it can be highly advantageous to build a micro-frontend solution. Micro-frontends really only come into their own if the scope of functions is very large. The separate development teams can each work independently, and the promises of the microservices are fulfilled. However, it is important to keep an eye on the technological diversity of the UI frameworks and minimise this, otherwise it will give rise to a kind of technology zoo that is no longer manageable.
It can be assumed that requirements for frontends, both from a functional and a non-functional perspective, will become more and more extensive. Therefore, the architecture must be scalable. A clear separation between domains is necessary to achieve a good balance between coupling and cohesion. Precisely because the principles and practices for microservices are established and known, it is advisable to transfer them to the frontends – but always to the degree necessary and appropriate. And for this, it’s imperative not only to know your functional and non-functional requirements, but to understand them very precisely.