How we scale our frontend applications using micro-frontends

Akash Aaron
Unibuddy
9 min readDec 14, 2021

--

At Unibuddy, we have a lot of products working together to deliver our customers the best experience. What started as a mono repo soon grew large in size with a lot of squads working on it. As our team grew, it became increasingly difficult to maintain our apps.

The move to microservices in modern tech development has inspired many game-changing ideas. One of them is Module Federation, which brought forward the potential to distribute our frontend apps into micro frontends. This allowed our squads to take up more ownership of their products and deliver features at a greater velocity than before.

What are micro frontends?

The concept of micro frontends has been on the rise, as tech companies seek to divide and conquer the way they write code and improve how squads take ownership. We’re aware that as a monolith app scales, it becomes increasingly difficult to maintain, especially across multiple squads working on the same app.

We can imagine micro frontends to be feature-based, where each squad may own a particular feature in a large app. Said squad shouldn't be concerned about how the app is deployed or how a change somewhere else may affect their feature. Ultimately, all the squad should care about is whether the feature they have worked on is delivered effectively to the user.

This concept allows us to break down our monolithic application into multiple smaller applications that appear as one single unified experience to our users.

Just as you would have services designed for specific tasks (microservices), you can have bits of frontend code dedicated to specific UI elements.

What is Module Federation?

Module federation was developed by Zack Jackson, who created it as a JavaScript architecture which was later adopted into Webpack 5 as a plugin. This architecture method allows for code/bundle sharing between two different and independent repositories/applications.

The code/bundle can be loaded dynamically on runtime, and any missing dependencies are managed by the host application. This allows for less code duplication and reduced bundle sizes.

Terminology

  • Host Application: a webpack build/application that is initialized on page load in the browser
  • Remote Application: a webpack build where part of it (modules) is being consumed by the host

Advantages of micro frontends

  • Micro frontends can scale better than monolithic apps
  • Teams can work on their own independent deployments and tooling. Its delivery will not affect the entire application. The changes will only reflect that part of a business process that it needs to cover.
  • Deployments are considerably faster as the feedback loop is much smaller now. PR’s can be reviewed more quickly as the overhead of side effects reduces considerably.
  • Codebases are much smaller and easier to maintain
  • Smaller apps can be tested more easily as we just need to test separate features
  • Failover is much easier for the host application as it can choose to simply not load the remote application in case something goes wrong. This prevents the breakdown of core apps.
  • Teams can choose to work on multiple frameworks. However, this needs to be undertaken mindfully and transparently to avoid de-standardization in an organization.
  • Smaller apps can upgrade packages and get rid of legacy code more easily.
  • It is easier to update or rewrite parts of code. Moreover, teams can experiment with new technology more freely than when tied to a larger app.

Drawbacks

  • As a large application is broken down into smaller chunks, it will become increasingly difficult to keep all developers working on the same standards and practices.
  • The build process and deployment of each micro frontend may be different, and hence the pipeline needs to be set accordingly for each.
  • Now that your core application is loading content dynamically, it can get harder to test the application as a whole. Each micro frontend app can be tested individually, but it is important to test the application from the perspective of an end-user, which becomes complex.

Best Practices

  1. Agree on methods of working: Your team must agree on a certain way of working on micro frontends and this should be conveyed to the rest of the business. It is important for the team working on the core application and the team working on the remote application to have consistent and well-designed agreements on workflows between different environments. The transition from code to production must be as seamless as that in a monorepo irrespective of how many teams may be working behind the scenes.
  2. Align business requirements: Think before breaking apps apart, does it serve any useful purpose and how will it provide an advantage to teams? Having a distributed architecture for linear business requirements makes no sense.
  3. Carry out proper testing: Be sure your apps are tested on multiple levels before production. It is always a good idea to have e2e testing for frontends apps and it becomes even more important in micro frontends as unit testing becomes difficult and complex.
  4. Follow some deployment standards: Teams should agree on certain pipeline methods to maintain standards across the organization. If each team has a different method, things can quickly spiral out of control and get hard to fix or debug quickly.
  5. Don’t overuse it: Think about this as designing microservices; too many and you have too many variables to manage. Always consider what value it provides to your team that is non-invasive to other teams. You want to solve problems and not create them by over-optimizing. The features that require a lot of changes and adaptability over time are a great fit for a micro frontend.

How we implement it

For our implementation, we will be setting up a simple combination of host and remote applications. The host application would be consuming a bunch of modules from a remote application and will render it on screen.

1. Folder Structure: It would look something like the example below. This looks like a mono-repo approach and is used only for this example. In real life, you may have two completely different repos with independent deployments implementing this.

We can define the index.html and package.json files as such which would be common across both applications

2. Setting up webpack for the remote application: We will be using a very basic webpack config to set up the remote application.

Some of the entries here are pretty basic to webpack and are required for react to work properly. What we are interested in is the ModuleFederationPlugin

Let’s have a look at the properties used in it.

a. name - This is the name of the scope of this application. This comes into play when we start consuming it in other applications and helps webpack figure out how to load remote scopes into the global scope of the host application.

b. filename - The name of the bundled file this application will spit out, which will be loaded by other applications (hosts)

c. exposes - All the modules that need to be exposed by this remote application

d. shared - Dependencies that need to be shared between apps. This is a really important parameter and we will cover this in detail later on.

3. Setting up webpack for the host application

Again, some basic webpack entries for react, let’s look at the ModuleFederationPlugin

a. name - Name of the scope of this application

b. remotes - An object containing key-value pairs of the name of remote scope and its URL. Looking at the remote webpack we see that the name of the remote scope was just remote, therefore we use the key-value pair which ultimately resolves to remote@http://localhost:3002/remoteEntry.js . The plugin is smart enough to resolve this URL and inject the remote bundled script into the host app. The helper getRemoteApplicationUrl function can be used to differentiate between local and production build processes (if you run this in CI)

c. shared - The shared dependencies between the two apps.

4. Setting up the core files in both applications

Looking at the src folder in both apps, we see that they have the same files. Which are

a. App.js - This is where we will write our components

b. index.js - Ok it is important we talk about this. You may be wondering why we simply cannot put the code from bootstrap.js here as you would do for any standard react application. We are doing this to ensure that react is always loaded before other chunks are loaded. Another way is to use eager: true for react in the plugin.

More info here

c. bootstrap.js - Entry code for react

Let’s have a look at how these would look for both our applications

  • Remote Application

The remote has a very basic and simple component with some styling.

  • Host Application

The only important thing to note here is how we import the remote module and lazy load it as a react component. The import can be looked at as

const RemoteApp = React.lazy(() => import("scope/module"));

After which you can treat this just like any react component. Just make sure you wrap it in a Suspense component.

5. Running it all together

cd into both the folders and install the dependencies using yarn install. Then do a yarn start to spin up the applications.

The host runs on localhost:3001 and the remote runs on localhost:3002

If you navigate to the remote app you should see the following:

If you navigate to the host app you should see the following:

Here, you can see that the host is consuming a chunk of code from a remote application that is in a completely different server.

VOILA 🎉

You have successfully implemented micro frontends. This was a pretty basic use case but there are complex solutions you can come up with to create the best possible development experience.

Dependencies (manage them carefully)

As you get into more complex micro frontend architectures with more and more packages being used, it will become crucial to manage shared dependencies optimally. You want to avoid loading large packages twice or have a context mismatch between downstream consumed apps.

Going back to our webpack configs, you will notice we use a shared property in the plugin. This is where you can define what dependencies you want to share.

This usually contains all core packages such as react , react-dom , react-router , @apollo/client , etc.

Moreover, you would also want to share packages that use context under the hood to prevent loading contexts from two different packages, effectively breaking your app.

Sharing packages also ensures that the remote bundle size is small and can be loaded quickly.

We use the singleton property to tell webpack to load the shared package only ONCE

Things to keep in mind

  • Use CSS modules in your remote. Globally importing CSS files in your remote will mess up the styles in the host when you load it.
  • Make sure that you load your remote only once. You won’t have control over the host application and it may have re-renders in the container where you want to load your remote.
  • Invalidate server cache for the remote on each deploy
  • Consider careful routing in your remote when the need arises. If you have an internal state (remote) being passed around in your routes, it may be a good idea to wrap it in a router inside the remote itself and expose the wrapper. If the remote can manage with the props being passed to it, you can be more modular and expose containers per route. Navigation in the host can remain in the host, but if you wanna go crazy you can federate that too.
  • Plan out configurations for the remote application. You may pass it down as props from the host or have it baked into the code.

Final thoughts

Module federation is an amazing and effective method to implement micro frontends, the challenge comes down to how teams in an organization coordinate sharing modules across apps. We’ve started to break out big apps into smaller chunks using this method and it has helped us cut down on development, feedback, and deployment time. We will be writing more articles on complex use cases including setting up a host application using Webpack 4!

Happy coding 🧑‍💻 👩‍💻

Link to the GitHub repo: https://github.com/unibuddy-labs/module-federation-webpack-5

--

--