Sustainable Angular Architectures

with Strategic Design and Monorepos - Part 2: Implementation

  1. Sustainable Angular Architectures
  2. Sustainable Angular Architectures
  3. Tactical Domain-Driven Design with Angular and Monorepos?

In the first part of this series, I've presented the idea of Strategic Design which allows to subdivide a software system into several self-contained (sub-)domains. In this part, I show how to implement those domains with Angular and an Nx-based monorepo.
For this, I'm following some recommendations the Nx team recently wrote down in their free e-book about Monorepo Patterns. Before this was available, I've used similar strategies but in order to help establishing a common vocabulary and common conventions in the community, I seek to use this official way.

Implementation with Nx

For the implementation of the defined architecture, a workspace based on Nx [Nx] is used. This is an extension for the Angular CLI, which among other things helps to break down a solution into different applications and libraries. Of course this is just one of several possible approaches. As an alternative, one could, for example, implement each domain as a completely separate solution. This would be called a micro-app approach.
The solution shown here puts all applications into one apps folder, and all the reusable libraries are grouped by the respective domain name in the libs folder:
Folder structure of the used monorepo
Because such a workspace consists of several applications and libraries that are managed in a common source code repository, there is also talk of a monorepo. This pattern is used extensively by Google and Facebook, among others, and has been the standard case for the development of .NET solutions in the Microsoft world for about 20 years.
It allows the sharing of source code between the project participants in a particularly simple way and also prevents version conflicts by having only one central node_modules folder with dependencies. This ensures that e.g. each library uses the same Angular version.
To install Nx you can use the following command:

npm install -g @nrwl/schematics

In order to create the CLI-based monorepo workspace, just leverage the create-nx-workspace command. Then, you can use ng generate to add applications and libraries:

create-nx-workspace e-proc cd e-proc ng generate app ui ng generate lib feature-request-product

Categories for Libraries

In their free e-book about Monorepo Patterns, Nrwl -- the company behind Nx -- use the following categories for libraries:

  • feature: Implements a use case using smart components
  • data-access: Implements data accesses, e.g. via HTTP or WebSockets
  • ui: Provides use case agnostic and thus reusable components (dumb components)
  • util: Provides helper functions

In addition, I'm also using the following categories:

  • shell: For an application that contains multiple domains, a shell provides the entry point for a domain
  • api: Provides functionalities exposed for other domains
  • domain: Domain logic like calculating additional expanses (not shown here). Can be consolidated with a corresponding data-access library to simplify the design.

To keep the overview, the categories are used as a prefix for the individual library folders. Thus, libraries of the same category are presented next to each other in a sorted overview.
Each library also has a public API through which it publishes individual components. On the other hand, they hide all other components. These can be changed as desired:

export * from './lib/catalog-data-access.module'; export * from './lib/catalog-repository.service';

Check accesses to libraries

To improve maintainability, it is important to minimize the dependencies between the individual libraries. The achievement of this goal can be checked graphically with Nx. For this, it provides the dep-graph npm script:

npm run dep-graph

If we just concentrate on the Catalog domain in our case study, the result looks as follows:
Catalog Domain with public API
In the case considered here, a few rules are used for the communication between libraries and these lead to a consistent layering. For example, each library may only access libraries from the same domain or shared libraries.
Access to APIs such as catalog-api must be explicitly granted to individual domains.
The categorization of libraries also has limitations: a shell only accesses features and a feature accesses data-access libraries. In addition, anyone can access utils.
To enforce such restrictions, Nx comes with its own linting rules. As usual, they are configured within tslint.json:

"nx-enforce-module-boundaries": [ true, { "allow": [], "depConstraints": [ { "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:shell"] }, { "sourceTag": "scope:catalog", "onlyDependOnLibsWithTags": ["scope:catalog", "scope:shared"] }, { "sourceTag": "scope:shared", "onlyDependOnLibsWithTags": ["scope:shared"] }, { "sourceTag": "scope:ordering", "onlyDependOnLibsWithTags": ["scope:ordering", "scope:shared"] }, { "sourceTag": "type:shell", "onlyDependOnLibsWithTags": ["type:feature", "type:util"] }, { "sourceTag": "type:feature", "onlyDependOnLibsWithTags": ["type:data-access", "type:util"] }, { "sourceTag": "type:api", "onlyDependOnLibsWithTags": ["type:data-access", "type:util"] }, { "sourceTag": "type:util", "onlyDependOnLibsWithTags": ["type:util"] },

{ "sourceTag": "name:ordering-feature", "onlyDependOnLibsWithTags": ["name:catalog-api"] } ] } ]

According to a suggestion from the mentioned e-book about Monorepo Patterns, the domains are named with the prefix scope and The library types are prefixed with kind. Prefixes of this type are only intended to increase readability and can be freely assigned.
In addition, this example also shows the domain Ordering which, according to the context mapping, has access to the CatalogApi. For this, the example uses a name prefix to make sure that only selected libraries are allowed to access the api.
The mapping between the projects and the library types and domains shown here takes place in the file nx.json:

"projects": { "ui": { "tags": ["type:app"] }, "ui-e2e": { "tags": ["type:e2e"] }, "catalog-shell": { "tags": ["scope:catalog", "type:shell"] }, "catalog-feature-request-product": { "tags": ["scope:catalog", "type:feature"] }, "catalog-feature-browse-products": { "tags": ["scope:catalog", "type:feature"] }, "catalog-api": { "tags": ["scope:catalog", "type:api", "name:catalog-api"] }, "catalog-data-access": { "tags": ["scope:catalog", "type:data-access"] },
"ordering-feature": {
"tags": ["scope:ordering", "type:feature", "name:ordering-feature"]
[...], "shared-util-auth": { "tags": ["scope:shared", "type:util"] } }

Alternatively, these tags can also be specified when setting up the applications and libraries.
To test against these rules, just call ng lint on the command line. Development environments such as WebStorm / IntelliJ or Visual Studio Code show such violations while typing. In the latter case, a corresponding plugin must be installed.
Don't expect that those rules always guarantee a beautiful dependency graph without overlappings as shown above. However, if you put each domain into a block diagram where each layer is only allowed to access layers beneath it, you can clearly see that you have got a clean and comprehensible architecture:
The result is a clean layered architecture
Moreover, everyone clearly sees where specific parts of the application are supposed to be found and the introduced rules prevent cycles, at least if APIs can only be accessed by features of other domains.

Conclusion and Evaluation

Strategic Design provides a proven way to break an application into self-contained domains. These domains are characterized by their own specialized vocabulary, which must be used rigorously by all stakeholders.
The CLI extension Nx provides a very charming way to implement these domains with different domain-grouped libraries. To restrict access by other domains and to reduce dependencies, it allows setting access restrictions to individual libraries.
Of course, it can be argued that an Angular client does not necessarily include domain logic. But the fact is that more and more logic can also be found on the client, especially with single page applications. Regardless, the outlined ideas of Strategic Design have proven to be extremely useful for getting a good cut.