The Enterprise Monorepo Angular Patterns

Leverage the full potential of Nx with Eslint rules and the Enterprise Monorepo Patterns

cover-image
portrait-image

Nx is a commonly used tool for monorepositories and is the top dog in the Angular community when it comes to building enterprise applications. Nx has many awesome features that help with scaling enterprise applications, but the greatest feature of them all is the nx/enforce-module-boundaries linting rule (also often referred to as access restrictions). When set up correctly, it allows for enforcing architecture rules and for automated validation with the linter. But these access restrictions assume a very specific structuring, which follows fine-grained horizontal and vertical slicing.

Horizontal Slicing

Horizontal Slicing is obviously very important in every enterprise application, independent of the enterprise monorepo patterns. It is just about slicing a big monolith into sub-systems, often referred to as domains or subdomains, especially if you follow principles of Domain Driven Design, such as Strategic Design, which gives some very useful tools to find a good horizontal slicing. In the case of an insurance CRM, which manages claims, contracts, customers, complaints and notifications, we would split this system into said subdomains.

result

Vertical Slicing

Now, considering the before identified domains, we can slice the application even further on a vertical level. We can slice each domain into libraries marked as either Feature, UI, Data-Access, Util, API. That means, that each domain is just a directory inside an Nx monorepo and contains many libraries which have a clear separation. On the one hand this vertical separation is great, because it gives a good structure and tells you exactly where to put what. But that is by far not all it has to offer, because the slicing also allows for setting up access restrictions such that we can control the dependency flow. For example, a UI library can not depend on a Feature or API library.

result

But hold on a second. What do these library types actually mean?
- Feature contains smart components.
- UI contains dumb components.
- Data Access contains entities, logic, services, state management.
- Util contains helper functions.


Splitting these things up into distinct libraries makes a lot of sense, when using Nx, because each library can be marked with custom tags in the project.json and in combination with linting rules, we can specify dependency rules based on these tags. That way, it is possible to make a rule that states that a UI library is not allowed to depend on a Feature library.


Another important library type is the API, which does not contain anything and instead only acts a proxy that exposes just a few things. Usually, a domain would not be allowed to access libraries from another domain, but with the API, we can open up a very small door that exposes the minimum, such that the domains have little dependencies on each other. An alternative solution to sharing dependencies would be to use a Shared Kernel, which is its own domain, but with the special rule, that every domain is allowed to depend on the shared domain. Although, the shared domain could be used to share dependencies, I advise using APIs because if the shared domain is growing bigger and bigger, the point of the enterprise monorepo patterns are defeated.

Eslint Rules

With the tags in the project.json set up, we now have to apply dependency constraints in the eslintrc.json. Inside the @nrwl/nx/enforce-module-boundaries you just have to add a rule for each tag. Therefore for each tag you have to create such an entry:

[...]
{
  "sourceTag": "type:ui",
  "onlyDependOnLibsWithTags": [
    "type:data-access",
    "type:util"
  ]
},
[...]

All the rules combined in the Insurance Portal example look like the following. Also, have a closer look on the rules for "domain:contract" because it extends the common pattern and in addition access to certain API libraries.

"@nrwl/nx/enforce-module-boundaries": [
  "error",
  {
    "enforceBuildableLibDependency": true,
    "allow": [],
    "depConstraints": [
      {
        "sourceTag": "type:app",
        "onlyDependOnLibsWithTags": [
          "type:api",
          "type:feature",
          "type:ui",
          "type:data-access",
          "type:util"
        ]
      },
      {
        "sourceTag": "type:api",
        "onlyDependOnLibsWithTags": [
          "type:ui",
          "type:data-access",
          "type:util"
        ]
      },
      {
        "sourceTag": "type:feature",
        "onlyDependOnLibsWithTags": [
          "type:ui",
          "type:data-access",
          "type:util",
          "type:api"
        ]
      },
      {
        "sourceTag": "type:ui",
        "onlyDependOnLibsWithTags": [
          "type:data-access",
          "type:util"
        ]
      },
      {
        "sourceTag": "type:data-access",
        "onlyDependOnLibsWithTags": [
          "type:util",
        ]
      },
      {
        "sourceTag": "domain:shared",
        "onlyDependOnLibsWithTags": ["domain:shared"]
      },
      {
        "sourceTag": "domain:claim",
        "onlyDependOnLibsWithTags": ["domain:claim", "domain:shared"]
      },
      {
        "sourceTag": "domain:contract",
        "onlyDependOnLibsWithTags": [
          "domain:contract",
          "domain:shared",
          "domain:claim/api-contract",
          "domain:complaint/api-contract"
        ]
      },
      {
        "sourceTag": "domain:notification",
        "onlyDependOnLibsWithTags": [
          "domain:notification",
          "domain:shared"
        ]
      },
      {
        "sourceTag": "domain:complaint",
        "onlyDependOnLibsWithTags": [
          "domain:complaint",
          "domain:shared"
        ]
      },
      {
        "sourceTag": "domain:customer",
        "onlyDependOnLibsWithTags": [
          "domain:customer",
          "domain:shared",
          "domain:notification/api-customer"
        ]
      }
    ]
  }
]
eslintrc.json

If you now violate one of the dependency constraints you will get an immediate linting error. Both from your IDE, and through an automated nx lint.

result
linting error inside UI library

Nx Graph

Another really awesome tool of Nx is the interactive Nx Graph, which shows you all the dependencies very intuitively. Grouping by folder, we can get a good image of the current architecture of our application and we are even able to focus specific libraries. With Nx 15.3.0, we can now even visualize the Task Graph, which might be of big interest to you, if you use buildable libraries and a remote Nx cache. Read more on Incremental Builds .

result
nx graph

II. Enterprise Monorepo Angular Patterns

Originally, the Enterprise Monorepo Angular Patterns had been introduced in this book , by Nitin Vericherla and Victor Savkin.

Conclusion

In conclusion, the Enterprise Monorepo Angular Patterns are well-established and a best practice for long-lived enterprise applications. That's because it enables highly independent sub-systems (or domains) and enforces linting rules that lint for architectural violations. All in all, when set up correctly, it enforces architecture rules and promises a good separation that is hard to break. The next logical step would be to set up a remote Nx cache and make the libraries buildable, such that the build performance is drastically increased. Stay tuned for further reads on this topic!

GitHub Repository

Comments