The concept of Micro Frontend Architecture

Sherif Amgad Nabih
Qrios
Published in
7 min readSep 29, 2021

The concept of Micro Frontend Architecture is to think about a web application as a composition of features owned by different independent teams. Each team having a distinct area of business that specializes in

And these are the main points that promotes micro frontends architecture to be used in every project:-

  1. Be Technology Independent — Each team should choose and upgrade stack without having to coordinate with other teams. Custom elements help to hide implementation details while providing a neutral interface to others.
  2. Isolate Team Code — Never share a runtime, even if teams use the same framework. Build an independent application self-contained. Do not rely on shared state or global variables.
  3. Create Team Prefixes — Use naming conventions where isolation not possible yet. Namespace CSS, Local Storage, Events and Cookies to avoid collisions and clarify ownership.
  4. Favor Native Browser Features over Custom APIs — Instead of building a global PubSub system use browser events for communication. If there is a need to build a cross-team API, try to keep as simple as possible.
  5. Build a Web design that is Resilient — The features should be useful, even if JavaScript failed or do not get executed. To improve perceived performance use universal rendering and progressive enhancement.

And here as shown in the diagram the difference between monolithic frontend and micro frontends

Monolithic Frontends

Organisation in Verticals

Integration approaches

Given the loose definition above, there are many approaches that could reasonably be called micro frontends. In this section we’ll show some examples and discuss their tradeoffs. There is a fairly natural architecture that emerges across all of the approaches — generally there is a micro frontend for each page in the application, and there is a single container application, which:

§ renders common page elements such as headers and footers

§ addresses cross-cutting concerns like authentication and navigation

§ brings the various micro frontends together onto the page, and tells each micro frontend when and where to render itself

Server-side template composition

We start with a decidedly old approach to frontend development — rendering HTML on the server out of multiple templates . We have an index.html which contains any common page elements, and then uses server-side includes to plug in page-specific content from fragment HTML files:

<html lang="en" dir="ltr"><head><meta charset="utf-8"><title>Feed me</title></head><body><h1>🍽 Feed me</h1><!--# include file="$PAGE.html" --></body></html>

We serve this file using Nginx, configuring the $PAGE variable by matching against the URL that is being requested:

server {listen 8080;server_name localhost;root /usr/share/nginx/html;index index.html;ssi on;# Redirect / to /browserewrite ^/$ http://localhost:8080/browse redirect;# Decide which HTML fragment to insert based on the URLlocation /browse {set $PAGE 'browse';}location /order {set $PAGE 'order';}location /profile {set $PAGE 'profile'}# All locations should render through index.htmlerror_page 404 /index.html;}

This is fairly standard server-side composition. The reason we could call this micro frontends is that we’ve split up our code in such a way that each piece represents a self-contained domain concept that can be delivered by an independent team. What’s not shown here is how those various HTML files end up on the web server, but the assumption is that they each have their own deployment pipeline, which allows us to deploy changes to one page without affecting or thinking about any other page.

For even greater independence, there could be a separate server responsible for rendering and serving each micro frontend, with one server out the front that makes requests to the others. With careful caching of responses, this could be done without impacting latency.

This example shows how micro frontends is not necessarily a new technique, and does not have to be complicated. As long as we’re careful about how our design decisions affect the autonomy of our code bases and our teams, we can achieve many of the same benefits regardless of our tech stack.

Build-time integration

One approach that we sometimes see is to publish each micro frontend as a package, and have the container application include them all as library dependencies. Here is how the container’s package.json might look for our example app:

{"name": "@feed-me/container","version": "1.0.0","description": "A food delivery web app","dependencies": {"@feed-me/browse-restaurants": "^1.2.3","@feed-me/order-food": "^4.5.6","@feed-me/user-profile": "^7.8.9"}}

At first this seems to make sense. It produces a single deployable Javascript bundle, as is usual, allowing us to de-duplicate common dependencies from our various applications. However, this approach means that we have to re-compile and release every single micro frontend in order to release a change to any individual part of the product. Just as with microservices, we’ve seen enough pain caused by such a lockstep release process that we would recommend strongly against this kind of approach to micro frontends.

Having gone to all of the trouble of dividing our application into discrete codebases that can be developed and tested independently, let’s not re-introduce all of that coupling at the release stage. We should find a way to integrate our micro frontends at run-time, rather than at build-time.

Run-time integration via iframes

One of the simplest approaches to composing applications together in the browser is the iframe. By their nature, iframes make it easy to build a page out of independent sub-pages. They also offer a good degree of isolation in terms of styling and global variables not interfering with each other.

<html><head><title>Feed me!</title></head><body><h1>Welcome to Feed me!</h1><iframe id="micro-frontend-container"></iframe><script type="text/javascript">const microFrontendsByRoute = {'/': 'https://browse.example.com/index.html','/order-food': 'https://order.example.com/index.html','/user-profile': 'https://profile.example.com/index.html',};const iframe = document.getElementById('micro-frontend-container');iframe.src = microFrontendsByRoute[window.location.pathname];</script></body></html>

Just as with the server-side includes option, building a page out of iframes is not a new technique and perhaps does not seem that exciting. But if we revisit the chief benefits of micro frontends listed earlier, iframes mostly fit the bill, as long as we’re careful about how we slice up the application and structure our teams.

We often see a lot of reluctance to choose iframes. While some of that reluctance does seem to be driven by a gut feel that iframes are a bit “yuck”, there are some good reasons that people avoid them. The easy isolation mentioned above does tend to make them less flexible than other options. It can be difficult to build integrations between different parts of the application, so they make routing, history, and deep-linking more complicated, and they present some extra challenges to making your page fully responsive.

Run-time integration via JavaScript

The next approach that we’ll describe is probably the most flexible one, and the one that we see teams adopting most frequently. Each micro frontend is included onto the page using a <script> tag, and upon load exposes a global function as its entry-point. The container application then determines which micro frontend should be mounted, and calls the relevant function to tell a micro frontend when and where to render itself.

<html><head><title>Feed me!</title></head><body><h1>Welcome to Feed me!</h1><!-- These scripts don't render anything immediately --><!-- Instead they attach entry-point functions to `window` --><script src="https://browse.example.com/bundle.js"></script><script src="https://order.example.com/bundle.js"></script><script src="https://profile.example.com/bundle.js"></script><div id="micro-frontend-root"></div><script type="text/javascript">// These global functions are attached to window by the above scriptsconst microFrontendsByRoute = {'/': window.renderBrowseRestaurants,'/order-food': window.renderOrderFood,'/user-profile': window.renderUserProfile,};const renderFunction = microFrontendsByRoute[window.location.pathname];// Having determined the entry-point function, we now call it,// giving it the ID of the element where it should render itselfrenderFunction('micro-frontend-root');</script></body></html>

The above is obviously a primitive example, but it demonstrates the basic technique. Unlike with build-time integration, we can deploy each of the bundle.js files independently. And unlike with iframes, we have full flexibility to build integrations between our micro frontends however we like. We could extend the above code in many ways, for example to only download each JavaScript bundle as needed, or to pass data in and out when rendering a micro frontend.

The flexibility of this approach, combined with the independent deployability, makes it our default choice, and the one that we’ve seen in the wild most often. We’ll explore it in more detail when we get into the full example.

Run-time integration via Web Components

One variation to the previous approach is for each micro frontend to define an HTML custom element for the container to instantiate, instead of defining a global function for the container to call.

<html><head><title>Feed me!</title></head><body><h1>Welcome to Feed me!</h1><!-- These scripts don't render anything immediately --><!-- Instead they each define a custom element type --><script src="https://browse.example.com/bundle.js"></script><script src="https://order.example.com/bundle.js"></script><script src="https://profile.example.com/bundle.js"></script><div id="micro-frontend-root"></div><script type="text/javascript">// These element types are defined by the above scriptsconst webComponentsByRoute = {'/': 'micro-frontend-browse-restaurants','/order-food': 'micro-frontend-order-food','/user-profile': 'micro-frontend-user-profile',};const webComponentType = webComponentsByRoute[window.location.pathname];// Having determined the right web component custom element type,// we now create an instance of it and attach it to the documentconst root = document.getElementById('micro-frontend-root');const webComponent = document.createElement(webComponentType);root.appendChild(webComponent);</script></body></html>

The end result here is quite similar to the previous example, the main difference being that you are opting in to doing things ‘the web component way’. If you like the web component spec, and you like the idea of using capabilities that the browser provides, then this is a good option. If you prefer to define your own interface between the container application and micro frontends, then you might prefer the previous example instead.

Choosing the right approach always depends on your business case and your technology preference

Finally I would like to stress on some of the great benefits of micro frontends:-

1. Micro frontends support code and style isolation, an individual development team can choose their own technology The development and deployment speed is very fast.

2. Micro frontends help in Continuous Deployment.

3. The testing becomes very simple as well as for every small change, don’t have to go and touch the entire application.

4. Front-end renovation — Improving new things becomes easier.

5. High Resilience and Better Maintenance.

6. Support code and style isolation

Conclusion

As frontend codebases continue to get more complex over the years, we see a growing need for more scalable architectures. We need to be able to draw clear boundaries that establish the right levels of coupling and cohesion between technical and domain entities. We should be able to scale software delivery across independent, autonomous teams and that’s where Micro front ends shines

--

--