Optimizing monolithic projects with libraries.
In the exciting world of web development with Angular, we face challenges when dealing with growing monolithic projects. This article explores common problems and presents a reasonable solution to enhance efficiency and flexibility in developing large-scale web applications.
Challenges of Monolithic Projects
- Compilation Time and Testing Cycle:
As code grows within a single project, we encounter an increase in compilation time and a slowdown in the testing cycle. This can negatively impact productivity and the responsiveness of the development team, potentially leading to the abandonment of test adoption during development.
- Module-less Architecture:
The new module-less architecture in recent versions of Angular greatly facilitates development and lowers the entry barrier for new programmers. However, removing the boundaries modules provide can create chaos where any artifact is visible throughout the application. This allows for complicated dependencies and hinders the implementation of robust software architectures.
The Solution: Specialized Libraries with Angular
The proposed solution to overcome these challenges is based on the creation of specialized libraries, replacing the role of traditional modules with greater flexibility and efficiency.
+ Test-Driven or Test-Supported Development:
Each library can be compiled and tested independently, even with specific tools based on its content and technological foundation. This facilitates development and speeds up feedback cycles.
+ Abstraction and Hiding Details:
Each library exposes functionalities while hiding implementation details. This well-established practice simplifies refactoring and aids in issue detection.
+ Dependency Rules:
Dependency rules can be established between libraries, allowing the creation of hierarchies in line with common software architectures, such as layered or clean architecture.
The proposed structure facilitates adaptation to Micro-front-end architectures and coexistence with other technologies or Angular versions in the long term.
Technical and functional division
I have based the proposed folder structure on the one presented in my previous article for medium-sized applications:
File and folder structure for Angular applications
Having a good scaffolding is the foundation for successful development. Here are some guidelines for naming and…
Tags for Feature-Technical Division
In that post, I have encouraged group code by functionality. This is commonly referred to as vertical slicing. But there is also the possibility of physically reflecting the technical division in libraries, horizontal layering.
Those two dimensions are valid approaches for dividing your monolith in libraries. But, most important is that you can use them to establish a set of rules among your libs.
To do so, I assign a tag name to each concept: horizontal (technical type) and vertical (functional scope) division. Nx allows you to apply these tags to any project.
Type and Scope can be represented in a table like this simplified example, highlighting the direction of allowed and restricted dependencies at the eslint rules level.
Recommended Tools: Nx.dev
Tools beyond the Js/Ts standard are needed to implement these rules. Not even Angular offers anything specific. You need to use plugins for es-lint, such as Sheriff.
However, currently, I recommend using tools from Nx.dev, which significantly enhance the developer experience. Among other things, Nx allows you to apply these tags to any project and highlight the direction of dependencies, allowed and restricted, at the level of eslint rules.
Practical Example: GitHub Lab
This is how it started…
# Create an empty repository [nx-lab]
npx create-nx-workspace@latest nx-lab --preset=empty
# Install Angular plugin
npm i -D @nx/angular
# Generate Main Application [activity-bookings] [lab]
npx nx g @nx/angular:app activity-bookings --bundler=esbuild
--e2eTestRunner=none -p=lab -s -S --style=css --ssr
--unitTestRunner=none -t --tags=type:app
This is the result in the VSCode of the example’s multi-project mono-repository.
At the eslint file, pay special attention to the section where project dependency limits are set, facilitated by Nx tools.
Granularity of Libraries
When creating libraries, granularity is critical. You can start with three large libraries: core, routes, and shared, but it is advisable to further divide them according to project needs.
Library per Main Route:
Having at least one library for each main route is recommended to facilitate management and maintenance. You can continue slicing and creating specialized libraries for each page from here.
You can even treat each functional route as a mini-application with its specialized UI, Domain, and Data service libraries.
Shared Container Division:
I recommend subdividing the shared container. First, extract features like logging or security, following the principle of maintaining cohesive and potentially reusable solutions. This is just another functional vertical division.
Specialized Testing Strategies
Technical divisions are also interesting as they allow the use of more specific tools, especially in the testing domain. For example, some libraries can focus on components, while others have no relationship with the framework.
Visual Testing with Cypress:
Libraries specialized in the presentation layer expose Angular components. Testing the UI in a traditional unitary way can be a pain.
But for some time now, they can be visually and user-friendly tested with Cypress. With the advent of component testing, they have provided an efficient and enjoyable development experience.
For this reason, I usually extract my dumb components to a library, to which I then add Cypress as a component test framework.
# shared ui library
npx nx g @nx/angular:library shared/ui -c=OnPush
--importPath=@lab/ui -p=lab --projectNameAndRootFormat=as-provided -s
--skipTests --style=css --unitTestRunner=none -t
# Component testing with cypress for ui library
npx nx g @nx/angular:cypress-component-configuration --project=ui
Pure Libraries for Data and Business Logic:
In a broad context, a domain refers to the specific area or topic on which an application or system is being built. Extracting data definitions and business logic to pure libraries, without contamination from any framework, allows the use of standard unit testing tools and is potentially reusable between technologies.
If you opt for that, this domain lib will be the core of a clean architecture. In my experience, most front-end developments don’t take it to the extreme due to the complexity of abstraction and dependency inversion they require.
npx nx g @nx/js:lib shared/domain --bundler=esbuild
# Unit testing with jest for domain library
npx nx g @nx/jest:configuration --project=domain
All those libraries can be spotted in a graph generated by Nx.
Validation in Large-Scale Projects
In my experience as a trainer and consultant, I have seen that this solution has been tested in developing projects for large companies.
It demonstrates effective scalability for any size or desired architecture, including Micro-front-end and clean architectures.
At the same time, it retains simple concepts of smaller applications: core, routed, and shared.
I hope these ideas contribute to the clarity and effectiveness of your developments. Please let me know if there’s anything else I can help with or correct!
Health and good luck.
Elevating code quality.