Modular Monoliths with Nx
Building scalable Angular applications


Frontend architecture is a crucial part of every application. It is the foundation for maintainable and scalable applications. In this blog, I will show you how to build scalable Angular applications using Nx. We will learn about monorepositories, Nx, and the Enterprise Monorepo Pattern. We will also discuss best practices and patterns for building scalable Angular applications..
But why should you even care about frontend architecture? Why not just build a regular monolith?
Well, because if you do so, over time, things just evolve and get messy. You will end up with a big ball of mud. And it will be hard to maintain and extend your application. You will have a hard time onboarding new developers. And you will have a hard time scaling your application. And you will have a hard time scaling your team. And you will have a hard time scaling your organization.
Therefore, a better approach would be to build a modular monolith. A modular monolith is a monolith that is built with modularity in mind. Hence, we get the simplicity of a monolith, but with a lot more maintainability and scalability.
By modularizing the code, changes become more predicatble, because the scope of a change is limited to its module.
Talk - Modular Monoliths with Nx
Monorepos and Nx
A monorepo is a single repository that contains multiple projects. In contrast to a polyrepo, where each project has its own repository.
This is a big deal, because in an enterprise environment, you usually are dealing with loads of dependencies and it is not uncommon, that you encounter ugly version mismatches. And this is where monorepos come into play. Because in a monorepo, you can easily manage all your dependencies in one place.
In an integrated monorepo, all apps and libraries included in a monorepo share the same
package.json and
node_modules.
This means, that all apps and libraries share the same version of Angular, RxJS, NgRx and basically all third-party dependencies in the project.
Therefore, you will never have version mismatches again. And you will never have to deal with dependency hell again.
On the other hand, you will have to organize migrations carefully, because you will have to migrate all apps and libraries at once.
This is a challenge for your organization, but it is definitely worth it.
Since, all libs and apps are in one place, you no longer have to distribute and consume them via NPM.
Instead, you can just import them via relative paths. So you no longer have the burden of thinking about versioning and publishing. All you have to do in a monorepo, is a simple import statement.
This means, that your latest checkout of your master/main branch is always the latest version of your application in a trunk based development workflow.
All the above mentioned benefits may make you wondering why not everybody is already using monorepos. Well, the answer is simple. Because monorepos are hard to manage.
In a monorepo, an app is usually split into many tiny libraries, which are then imported into the app. This makes for better module boundaries, but managing a three-digit number of libraries still remains a challenge. Such an amount of projects has a huge impact on the performance of linting, testing, building, and serving.
But Nx solves this problem. Nx is a set of tools and libraries that help you to build, test, and serve your applications. It is built on top of the Angular CLI and provides a lot of additional functionality. Interactive dependency graphs. Computation caching. Distributed task execution. Generators. Executors. And much more.
But the most underrated feature of them all are Module Boundaries, which can be used to architect applications with strict boundaries and architectural validations.
Module Boundaries
First of all, let's discuss why
NgModule's are not strict boundaries and a better approach is needed.
Alhtough, many applications use
NgModule's to organize their code and separate concerns, they are not guaranteed to be used as intended.
For, example a component, which is declared but not exported in an
NgModule can still be used outside of the
NgModule. Even though, the intention clearly was to use it only inside of the
NgModule. By going through the file-system, the component can be lazy-loaded and dynamically bootstraped - hence breaking the boundary.
Obviously, in a standalone world without any
NgModule's, we need a different solution either way to enforce architectural boundaries.

Nx provides the nx/enforce-module-boundaries rule, which can be adapted in the .eslintrc.json file. In there, we can explicitly define which tags are allowed to be imported by which other tags.
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "scope:boundary-a",
"onlyDependOnLibsWithTags": ["scope:boundary-a", "scope:shared"]
},
{
"sourceTag": "scope:boundary-b",
"onlyDependOnLibsWithTags": ["scope:boundary-b", "scope:shared"]
},
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
}

Enterprise Monorepo Pattern
The Enterprise Monorepo Pattern stems from the Enterprise Angular Monorepo Patterns book by Nx, which covers multiple patterns, but since the pattern I will be showing in this section is the most popular pattern from this book - I would just title it the Enterprise Monorepo Pattern.
In fact, I will introduce you to a slight variation of the traditional pattern, which solves a few issues, that I encountered in the past on real-world projects.
Let's consider the following example. We have an application, which is called
insurance-portal. It is a CRM which is used to manage claims, contracts, complaints and customers.



- Routes are used to define the routing configuration of a domain. By having a dedicated library type for just routes makes it possible to compose multiple features on one page.
- API libraries are private API's which act as a gateway between domains. They are used to share the least minimal amount of code between domains.
- Feature libraries are used to implement a specific feature. They are context-aware components, which are not meant to be reused.
- UI libraries are used to implement reusable components, which are not context-aware. They are meant to be reused.
- Data Access libraries are used to implement state, business logic and services, which are used to fetch data from the backend. They usually hydrate the feature layer with data and business logic.
- Utils libraries are used to implement utility functions, which are used by multiple other libraries. An example would be a date formatter.
- Models libraries are used to implement models, which are used by multiple other libraries. An example would be a claim interface definition.
There is a clear dependency flow from top to bottom. Meaning that libraries are allowed to depend on libraries down below, but not upwards. For example, a feature library is allowed to depend on a ui library, but not vice versa. This is crucial to ensure that the smart vs dumb component principle is not violated.
For more information about the Enterprise Monorepo Pattern, I would recommend you to read this blog post I did a while ago on the more traditional approach: Enterprise Monorepo Angular Patterns
Definitely make sure to checkout the repository linked below containing all the code examples from this blog post (and a lot more).