Breaking up the Frontend Monolith into smaller libraries part I
What I use?
- Design Pattern: Strangler pattern
- Technique: Micro frontends
- Integration approaches: Build-time integration via Web Components
- Package custom elements (also called Web Components) from Angular framework: Angular elements
- Repository: Monorepo
- Development tool: Nx
My company would like to rewrite the application from the existing monolith legacy application (AngularJS). There are many good reasons why we need to do that:
- Long Term Support: The framework (AngularJS) will end of LTS in December 2021. There is no easy way to migrate AngularJS application to Angular without rewriting.
- Software Regression: The legacy application doesn’t have the unit tests (or have very small amount). There is no guarantee that the new features being added in would cause the regression bugs or not. This causes the burden to the manual testing, where it is not practical to retest the whole application with all possible scenarios, and UI testing, where it takes a lot of time to run and it is not reliable — flaky. This regression problems could be solved by writing more unit tests to increase more test coverage. However, this leads to another dilemma — writing more unit tests requires refactoring and refactoring could cause regression bugs because we don’t have enough unit test coverage.
- Velocity: From the above, the developers need plenty of time to read and understand the old codes. The QA requires plenty of time to do manual testing and analyze the result of UI testing. The team starts to create the paper process to make sure that all team members make the proper analysis before changing the code.
- Better performance and security: Upgrading to the new Angular version means that the application is under LTS which means that if there is any security vulnerabilities, they will be fixed. Also, Angular is much faster than AngularJS due to algorithm for data binding and a component-based architecture. This will improve user experience.
There are many approach to rewrite the application — The big-bang is one approach but it seems not practical as the application is big, complex, and covers many business lines — the time spent on rewriting the whole application in one shot would be more than years and the situation would be more complex when we need to handle the new requirements and bug fixing and reflect to the new system.
I decide to use the strangler pattern to gradually replace some specific parts of the legacy application with the new one. The end vision is that the legacy application is decomposed by being rewritten into the new libraries. The scope and responsibilities of the legacy applications will be less and less. At the end, it will mainly acts as the application shell to manage the communication and integration among the micro frontends. If we reach at that stage, we will finally replace the last piece of the legacy application easily.
Note: The methodology we are using for subdividing the monolith to small and maintainable parts is not part of this article. If you are interested, please read Sustainable Angular Architectures with Strategic Design And Monorepos. In that article, Manfred will describe how DDD (Domain Driven Design) helps breaking down of a large system into individual (sub-)domains and their design.
Why a monorepo?
A monorepo is a single respository that stores all codes and assets for every project.
In this sense, a monorepo means a single repository being created to store all libraries being extracted, decoupled and rewritten from the certain parts of the monolith legacy one.
There are benefits of using monorepo which are described in the book,
Angular Enterprise Monorepo Patterns. I will reflect these benefits into my perspective as followings:
- Increase visibility: Monorepo consist of multiple libraries in the same repository, these libraries might belong to many different teams. In this practice, each team can have more visibility on changes other teams are making. Personally, it is easier for me to look over the shoulder and verify that each library provides the proper API contract, is loosely-coupling, and defines proper scope and boundary.
- Reuse code directly: The library could depend on other libraries (shared library). In monorepo practice where all libraries live under the same repository, the library can refer to other library directly instead of packaging and deploying to private npm and adding to the dependent project dependencies.
- Ensure a consistent version of dependencies: All libraries will depend on one single version of dependencies (package.json file at root level).
It contains schematics to help with code generation in the projects to ensure consistency.
It contains tools to help with enforcing linting rules and code formatting.
It allows you to visualize dependencies between apps and libraries within the repository.
It allows commands to be executed against code changes: any uncommitted changes, comparison between two specific git commits, comparison against another branch, etc. These commands can test, lint, etc. only the affected files, saving us a lot of time by avoiding files that were not affected by our changes.
It includes utilities to help with race conditions in Angular applications