Dieser Artikel ist auch auf Deutsch verfügbar
System boundaries tend to be a nuisance from the user’s point of view. Thus the motivation for the modularization of systems is purely technical and should not hinder of frictionless workflows without any major loss of context.
So if we have cut up our systems “for reasons”, we have to bring them together in the frontend for our users. This can happen at different points:
- Static overarching things like layouts can be integrated during the build or deployment.
- With dynamic content, the backend can obtain necessary information from other systems. This can mean making direct calls to another system to process the request in question. In this case, a response from another system is needed in order to answer your own request. This could also mean asynchronous interactions between servers that are decoupled from the request/response cycle, such as a feed-based replication of data.
- From the author’s point of view, however, the silver bullet is integration in the frontend, specifically in the user’s browser. This type of integration is usually more lightweight, has better performance and has the fundamental advantage that our own systems (i.e. the backends) do not have to wait for another system (resulting in a delay in our own response) because the integration takes place on the user’s end device.
Much has already been said about the first two options, so this article will address the third option: frontend integration. As so often happens in IT, there is not only one option for implemeting frontend integration, but many. The following sections provide a list of some of the options (disclaimer: the list does not attempt to be exhaustive).
The list should begin with today’s quasi-standard. This is the monolithic single-page app (SPA) written in the frontend framework that was trending during the year that the app was created.
In my opinion, the ongoing microservices hype was primarily a hype in the backend. This means that many microservice architectures arose without consideration for anything apart from the backend. If you then find time to deal with the “little problem” of the frontend, and take the simultaneous hype for single-page apps at face value, it seems logical to integrate the backends at the level of your APIs.
A Single Page App (SPA) is then built, typically with a special team of UI/SPA experts. Normally, this app will make Ajax calls to the REST APIs of the backend team.
What is convincing about this concept is that the web frontend is a completely independent system. It therefore fits in seamlessly with other consumer systems like the mobile client for iOS or the calls to a partner company, as any architect would wish when drawing it on paper or a flipchart.
Unfortunately, a closer look reveals some technical problems:
- Public APIs must all have cross-cutting issues such as authentication or security in general. However, this can usually be solved somehow with infrastructure.
- The naive approach to the orchestration of service calls against different backends over the internet is the opposite of highly performant. This is especially the case when it comes to mobile internet. Here there are no simple technical solutions, only architectural ones: The backend-for-frontend pattern helps out if the frontend team is allowed to build a backend system (typically using NodeJS). This special backend system takes over the orchestration of the actual backends and can thus be considerably more compact and communicate more efficiently with the frontend.
However, the main problem with this approach is not technical but rather one of organization: by having invented a cross-sectional frontend team, the fundamental idea of microservices is actually being violated. Ideally, microservices should customize teams and systems around business areas, which can never be the case with a cross-sectional team (otherwise it wouldn’t be a cross-sectional team).
As a consequence, it is difficult to imagine that a new feature could be implemented by one team alone. As a rule, each new functionality in the backend will require a corresponding feature in the frontend, and vice versa, in order for it to be usable at all.
- There is a strict separation between the backends and the one frontend.
- Monolithic single-page apps are “state of the art”.
- You inherit all the disadvantages of a monolith as well as all the disadvantages of still having several teams.
- SPA technologies are short-lived. The team becomes extremely dependent on a framework creator.
However, the organizational problem of a monolithic SPA can be addressed with technical means and some effort. To do this, the SPA is divided into several modules that are provided by the respective specialized teams. This means that there is still one Single Page App in exactly one framework version. However, this app optimally loads its functionality in the form of modules directly from the different systems and integrates them via framework-specific interfaces.
One option for executing this approach is Angular lazy Feature modules. In this case, the components of an application are distributed in various so-called Feature Modules and are loaded only when they are needed. It is also possible to load these modules from different sources. This mechanism can be used to load the feature modules from different backends only at runtime on the client. With a proper module interface, the teams then regain sovereignty over their functionality and are able to develop and roll out new features independently and wholly in the spirit of microservices.
With webpack-based projects, this can also be done via Lazy Loading. In principle, every modern module bundler and every modern SPA framework contains some mechanism for this. For example, this is the fundamental idea of the Polymer framework.
- You can still pick an SPA framework and almost never leave its world.
- The organizational implications of the monolithic approach can be partially resolved.
- Ultimately, such a system is a structured monolith with many disadvantages that a monolith in the backend also has. In the implementations of this variant known to the author, the upgrade of a common framework was as good as impossible, even in the case of a small version upgrade.
To make sure that a monolithic single-page app does not become too large, is is possible to try to keep the feature set of the SPA as small as possible. For example, an SPA for a dashboard could be built by an appropriate dashboard team and make use of the JSON backend APIs of the other systems. This SPA should then be as “flat” as possible, i.e., it should jump as quickly as possible into the frontend of the respective responsible system.
- This approach is particularly useful for dashboards or smaller menu widgets.
- The appeal that “the SPA should jump into the systems quickly,” will in practice probably fall on deaf ears and the SPA will instead grow naturally.
Complex technical DOM components
The Web Components Standard CustomElements would be suitable for this. With this standard, components can be defined that initialize themselves on the DOM without needing a framework. Such a component could then, for example, load data from the backend JSON APIs, manipulate the DOM, send events to other components, etcetera …
In the simplest case, these components are instantiated by server-side HTML. The individual backend systems can simply serve HTML and integrate its own components as well as the components from other systems by referencing them in their own HTML.
Components of this type can also be built using SPA technologies such as Angular, vue or React. However, this is only worthwhile if they still load quickly and do not have a very large “footprint”. In addition, modern SPA frameworks such as Angular are unfortunately not built with this in mind and often take control over the entire site, or, at the very least, the URL.
Other frameworks like Polymer or Stencil are actually built with this idea in mind. However, the decision of which framework to use will be strongly influenced by the size of the respective components, at least for public sites. In the case of Polymer, these can be in the three-digit kilobyte range when unzipped.
- Components are optimally completely independent of one another. This means, for example, that they are interchangeable or can be further developed independently.
- The components extend a foreign DOM in order to display themselves. Dealing with dependencies, e.g. CSS, is no trivial matter. The new API ShadowDOM can help by isolating the CSS, but this also gives rise to new questions.
- Unfortunately, due to their large footprint, common frameworks are often not suitable for developing such components.
By comparing the “complex technical components” approach to the SPA approaches, an important step has been made: We are integrating the backend systems, at least potentially, via HTML rendered on the server side. If all backends provide most of their functionality via server-side HTML, this makes a much simpler method of integration possible: the hyperlink.
If you want to integrate the functionality of another system into your respective system, you can just link it. The user thus switches to the other system and simply returns to the first one upon finishing their work there. If the look and feel of the target system is the same, the user will not even notice that there was a switch in the system. Of course, it is important that the link points sufficiently “deeply” into the target system so that the user goes directly to the place where it is possible for them to continue working.
However, the interesting question here is exactly where the user should be directed back to. A naive response may be permanently configured “entry points” in each application. However, this would probably not support the workflow of the user, as the user would first have to restore their context in the system upon return.
For this purpose, it is better to integrate a special parameter that all systems must know and handle correctly in the URIs and forms. This parameter usually contains the return URI to which one should “redirect” back to after successful execution of the sub-process in the original system.
The following questions must also be answered in the respective architecture:
- Should there be only one return parameter or several (e.g. in the case of success or cancellation)?
- Should a redirect URI in turn contain another redirect URL (cascading)?
- If necessary, do you want to use URI templates in order to be able to pass data to the originating system in the return URI?
- Links are probably the most widely used integration tool in the world. Links and return parameters are, therefore, easy to execute in almost any environment. It is not necessary to have more than a halfway RESTful server-side HTML frontend for this.
- The user experience of integration via links usually does not meet the requirements of our day and age.
- System changes via links or via return enable the “handover” of a frontend process from one system to another. What it unfortunately does not usually enable is the transfer of data, which is required for most transfers.
- Cascading return URIs pose a risk of creating spaghetti-like navigation (e.g. the user jumps into a system via a return address which in turn contains a return address, etc.)
Hypermedia with data transfer
If you want to solve the not-entirely-solvable problem of data transfer between systems via frontend, you can use HTML forms for this purpose. A backend can thus wrap data that it wants to transfer to another backend into a form that “points” to the other backend. If the user submits this form, the system change is completed and the target system receives the data.
Data that the user should not be able to manipulate, such as IDs or internal URIs,
could be embedded in
hidden fields within such forms.
However, it must then be ensured that this
data cannot be modified by the user, since they can be changed as desired
in the browser before being sent, for example, in the developer tools.
Therefore, a signature must also be added into the form on the server side
and validated on the target system.
If such a form consists exclusively of
hidden fields, i.e., no additional data input
by the user takes place, links can be used for this procedure
again and the information can be packaged in corresponding URI
parameters. Here it is still necessary to check the integrity
of the data here as well, for example, via signatures. This process is often found
in procedures for exchanging tokens for single sign-on solutions.
If the user is not even allowed to know the content which is to be transmitted, the confidentiality of the data must also be ensured. For this, encrypted data must be stored in the forms or links.
- Transmission of system-internal data in the case of system changes purely through the frontend becomes possible without requiring direct calls between the backends.
- Signature calculation of HTML form values must be executed the same way in all systems. It is essential that the recipients of such form data verify the signatures.
If simple links are not sufficient, you could try to integrate the functionality of other systems in the by integrating toher HTML into your own HTML. This is often called client-side transclusion.
Using small generic JavaSciprt snippets in the browser, content is loaded from other systems into its “own” (usually rendered by the server) DOM. Conceptually, a link to an external system is replaced with the content of the response when executing an HTTP GET on this link.
After some consideration of the existing requirements, these questions can usually be answered relatively quickly (see references).
- Technically, a very small client-side component is sufficient for client-side transclusion.
- In general, the backends do not have to be programmed much differently from the simple “link” variant and do not require any additional JSON APIs or anything like that.
- One blog post by the author that illustrates this approach in detail.
- One presentation on the topic by my colleague Franziska Dessart.
- Gustaf Nilsson Kotte wrote an article with a similar focus.
- ROCA h-embed is a simple CustomElements component for link replacement, i.e., replacing links with their content.
- pjax is the classic component for transclusion.
- Turbolinks is a slightly different approach extracted from Ruby on Rails.
Using the standardized “postMessage” method, IFrames can exchange messages with other IFrames or in the embedding window.
- Even multi-step server-side processes can be easily integrated.
- IFrames have a heavy weight because of the isolation of the resources. For this reason, many integrations on one page via IFrame is not an option (e.g. it isn’t possible to integrate many smaller UI components).
- Responsive IFrames_ are not a trivial problem. Thus CSS media queries relate to the width of the IFrames, which even appears logical at first glance. If the goal is to design the entire page with fixed breakpoints as is usual, this means that other breakpoints apply within the IFrames (if it is not 100% wide) than in the rest of the browser window.
- IFrames do not have automatic inner height. So, unfortunately, non-scrolling IFrames with a non-fixed aspect ratio are not trivial.