Module Federation

Replacing a Legacy Forms Engine

I was the project owner for Module Federation over multiple repos. We had been using a legacy forms engine that was beginning to become an impediment to development, since it required learning poorly documented custom forms logic. We needed a solution that would let us develop different repos simultaneously while being consumed by a main web platform. Module Federation with Webpack provided the solution. Using Module Federation, every repository could be turned into a bundle of JavaScript that was then consumed by the host.

Module Federation Architecture

The architecture was as follows. We had multiple repositories with full-featured Angular apps in them. These Angular apps were bundled using Webpack. During the build pipeline, an AWS code artifact was created and the .js file was uploaded to an AWS bucket. Afterwards, the main web platform could load these files. Because we needed to maintain compatibility between legacy forms apps as well as new Module Federation apps, every app was also registered on the backend as being either a module federation app or a legacy forms app. We had a service that would fetch all our apps and then fetch from the appropriate bucket.

Development and Organizational Impact

The gains from replacing a legacy app with a Module Federation app was immediate. We used to have to test our forms engine by running a separate demo environment from our SDK. This demo environment had to be seeded with data; it could not fetch data remotely. If something went wrong with the SDK build for whatever reason - a dependency broke or an Angular update changed something - the app had to be tested by building the entire thing and then using that artifact. This meant any change easily took 15 minutes simply to verify. Updating to Module Federation meant we could test things immediately locally with remotely fetched data. It also became easier to work on the repositories because we could use standard Angular. Any new employee could get started right away, instead of needing to rely on the only employee who knew the forms engine. This increased the organization's resilience and made it much faster and more pleasant to both add new features and debug old ones.

Scaling and Incremental Migration

Our goal was to replace all the legacy apps with Module Federation. We began by using the first project as a template, but this proved to be a problem. To this end, I found a script that had previously been used to make legacy forms apps and refactored it to create a template for migrating our legacy apps.

Because there were so many apps, we were not able to migrate all of them. However, sometimes we wanted to migrate parts of an app. To this end, I needed to devise a way to load only some components, rather than an entire app. After reading the documentation, I found that it was possible to load components without needing to have the entire app be migrated to pure Angular, and that we could inject data into these components. Using this, we were able to move certain pages and components to use Module Federation without needing to commit to migrating larger, more complex, and client-sensitive applications all at once.

Resolving Build and Runtime Issues

Working with Module Federation presented its fair share of challenges. As the owner, I had to familiarize myself with build pipelines and AWS, not just Angular. One issue I ran into involved the pipeline failing to properly name new versions of the app on build, meaning that the version in AWS buckets did not have the name that the platform app expected. I had to work closely with the backend team to resolve this kind of issue.

A major issue I ran into was a race condition. There were times where you would click on one app, attempt to switch to another, and you would find that the first app continued to load. This baffling issue turned out to be caused by a race condition due to how similar the chunks created by webpack were. Though changing how the chunks were determined helped, it did not solve the problem. A permanent solution was found through webpack configuration, where I found it was possible to disambiguate bundles by adding a unique determiner to them. This solved the race condition once and for all.

Proposed Monitoring and Automation

While working on the project, I did not stop thinking about ways to improve the Module Federation system. One idea I had came from some of the issues we ran into during deployment. There were times when an older version of an app was deployed, overwriting a newer version. Hunting down why this happened was tedious and not straightforward. It was also an issue because our testing of the Module Federation apps was entirely manual. I had to click on all the apps to check that they loaded correctly in the correct version. This tedious task was sometimes not checked by testers. Because we had multiple tenants with different versions of the apps on each environment, it was confusing and time-consuming to test all of them oneself.

I sketched a plan to use our state machine in order to test that the Module Federation solutions would load correctly. If a solution failed to load, it would send an e-mail or text to the stakeholders involved. This would not just allow me to deploy the correct version, but to immediately investigate why an incorrect version had been deployed. The automated testing would have spanned multiple environments and tenants. While automated testing of the Module Federation apps did not end up being a priority, I am still curious about developing ways to make debugging Module Federation easier.