Scaling From Modest Beginnings to a Monolithic Giant

At the outset, most projects are compact, straightforward, and pleasant to work on collaboratively.

For a modest Single Page Application (SPA), a modulith architecture frequently serves as a fast, efficient answer to the web product's needs.

A team of roughly 5-10 individuals can easily synchronize their efforts, enjoy a smooth developer experience, and steadily ship features without friction.

Suppose this application turns out to be a massive commercial success — a genuine cash cow for the organization, drawing substantial corporate interest.

The Initial Obstacle: Expanding the Workforce

With increased investment aimed at making the product faster, more robust, and broader in scope, the team size is expanded. Before long, 20 to 30 engineers are all contributing to the same codebase.

Collaboration starts to feel strained, prompting the larger group to fragment into smaller squads of around ten members each. These squads now own distinct modules or functional areas within the SPA, yet the project itself remains a monorepo SPA.

This growth brings its own kind of bureaucracy. Any new feature or shared library change that touches the whole system now demands consensus from every single squad.

The Second Obstacle: Limited Resources and Timelines

As time goes on, certain parts of the site become more prominent or central to the business. This leads to budget reapportionment and team reconfiguration, creating an uneven level of development velocity and capacity across squads.

Meanwhile, the preferred framework (Angular❤️) ships a brand-new major version packed with compelling enhancements. Some squads are keen to upgrade right away — especially since the new control flow syntax is so appealing — while others simply do not have the time or effort to spare. They cannot absorb the breaking changes or rework their code immediately.

The Third Obstacle: Framework Dependencies

As the product keeps thriving, the company wants to recruit even more talent and pour in additional resources. Yet a surprising hurdle presents itself:

❗Finding additional Angular developers proves extremely difficult❗. The company already employs many, but the broader job market offers few candidates, and even fewer with deep Angular expertise.

Bringing in engineers experienced in other frameworks and onboarding them is an expensive proposition.

The Situation Now

At this point, the project finds itself in a predicament. The end users perceive no slowdown — thanks to the modulith structure, the site still feels responsive and quick. However, the developer experience is steadily deteriorating.

The codebase has ballooned in size, resulting in slower build and deployment cycles, and local development has become noticeably less pleasant.

The problems

MFEs: A Viable Way Forward

Clarifying Responsibilities

Given the challenges outlined so far, it's time to consider the potential remedy — Microfrontends.

Let's see how MFEs might reshape the SPA discussed above. The initial task is to identify the Shell and assign roles to the remaining MFEs.

The Shell assumes responsibility for shared concerns like authentication, sparing each individual MFE from those duties.

Each squad, which previously handled a specific module or section, now takes ownership of its own dedicated MFE.

This shift naturally leads to dividing the SPA monorepo into several distinct repositories: one for the Shell, and one for each MFE. Each of these now functions as an independent application, complete with its own build and deployment pipeline.

Module Federation

To bring this to life, we can turn to Webpack Module Federation, leveraging the tooling provided by AngularArchitects.io.

☝️ Remember
As said, this lecture focuses more on the idea and concepts. For a technical solution, I recommend reading the series of post of AngularArchitects.io

At this juncture, we have already resolved:

  • Team Scalability: Each squad can operate autonomously, significantly lowering the odds of merge conflicts and inter-team friction.
  • Framework Flexibility: Individual teams can adopt alternative frameworks like React if that suits their needs better.

But does this approach help with the budget issue we raised earlier?

Leaning on Web Components

Web Components offer a way to isolate our Microfrontends even more thoroughly, mitigating the typical issues around namespace collisions, CSS scope, and shared dependency conflicts that can plague basic MFE setups.

This level of isolation allows the Shell and the various MFEs to run different Angular versions simultaneously.
For instance, the Shell could be running Angular 17, while an MFE with a tighter budget stays on Angular 15. These pieces can still work seamlessly together, communicating through the Web Component interface using Inputs and Outputs (with Outputs transforming into CustomEvent automatically), or via a bespoke messaging layer built upon custom events.

☝️ Important note
Module federations allows using shared services for sharing state but when using different versions of the framework this is not longer possible, because they have separated Angular Dependency Injection trees with different root scopes.

The Road Ahead

The shift to this architecture brings with it a fresh set of obstacles to navigate.

Standardization

With the introduction of numerous standalone artifacts — the Shell and each MFE — we leave behind the simplicity of a shared module within a single SPA context. To maintain consistency across UI components, styling, and other common elements, the creation of dedicated libraries becomes necessary. These libraries serve as the backbone for normalization across every artifact.

❗ Important!
I' am using Shared Modules as an example but they are not a good solution for sharing code.

The Size Dilemma

While individual bundles are now leaner, a new issue emerges. The bundle's footprint is now tied to versioning, and the inclusion of Web Components can dramatically inflate its size. In an ideal scenario where every artifact relies on Angular 17, the initial load fetches both the Shell and the core Angular 17 bundles. As the user navigates, each subsequent MFE can reuse these already-cached files, avoiding redundant downloads.

Contrast that with a more fragmented setup: a Shell and four MFEs on Angular 17, two on Angular 16, and three on Angular 15. As the user journeys through the application, they will trigger downloads for the distinct bundles of each Angular version, **leading to a heavier load on the client's resources.**

☝️ Important note
I have used Angular versioning as an example, but this affects to every single dependency, could be the framework, or any library.

Managing Contracts

Another critical aspect to manage is the interface between the Shell and the MFEs. Their interaction relies primarily on Inputs and Outputs, which are defined independently in each part. **These contracts demand meticulous definition** to prevent integration issues. The use of shared libraries can offer significant assistance in codifying these agreements.

The client's browser operates with a single URL, which must always accurately represent the current state of the screen. Consider a scenario where two MFEs are active simultaneously, each with its own internal routing logic. They will update and potentially overwrite the URL as they navigate independently, causing a mismatch between the address bar and the content displayed by the other MFE.

❗ Important!
Only the Shell and maximum one MFE should have access to the routing
☝️ But soon...
angular-architects is currently developing a library that allows multiple routers, the first stable version should be in the first quarter of 2024

Final Thoughts

To sum up, MFEs offer a powerful way to tackle a wide range of problems that often arise not from poor planning or structure, but from the inherent complexity of the product itself.

That said, this ecosystem is not without its own hurdles. For substantial projects, I believe this approach is essential, and with a robust and carefully designed architecture, these challenges can be successfully overcome.

I trust you found this exploration valuable, despite it being my initial foray into writing. Should any questions arise, feel free to reach out through my linked social profiles. Thank you for reading!

A special note of appreciation to MikeZks for the invaluable assistance in refining this piece.

Useful Resources