DEV Community

Cover image for Architects Delight: Enforcing Layers and Project Boundaries with Nx
Nacho Vazquez for This is Learning

Posted on • Updated on

Architects Delight: Enforcing Layers and Project Boundaries with Nx

Introduction

When I embarked on my journey as a software developer, I delved into the depths of software architecture. Renowned books such as "Clean Architecture" and "Domain-Driven Design (DDD)" provided valuable insights into concepts like project boundaries and software layers (both vertical and horizontal). Yet, what was sorely missing was a quick and efficient tool to enforce these principles.

Then, I discovered Nx, specifically its @nx/enforce-module-boundaries ESLint plugin, which has revolutionized how I enforce these boundaries.

The Hidden Side of the Iceberg

Interestingly, although the @nx/enforce-module-boundaries ELLint plugin is embedded in the generated code of every integrated Nx-Monorepo, it seems to fly under the radar when the conversation turns to Nx's merits, which are indeed numerous. Even Nx's video, "The Nx Iceberg," doesn't mention it, despite its focus on the tool's lesser-known features.

Several authors, including Manfred Steyer, Lars Gyrup Brink Nielsen, and myself, have delved into this subject. Still, many developers using Nx aren't utilizing this feature as much as possible. That sparked me to write this article highlighting Nx's power in enforcing project boundaries.

What are Project Boundaries and Layers?

Before we delve into enforcing project boundaries, it's essential to understand what they are. These rules or principles dictate how different parts of our software interact.

For instance, software architects often use philosophies such as onion architecture and clean architecture to define horizontal layers. These layers are then grouped into vertical slices or boundaries using principles like the strategic design from Domain-Driven Design.

Without automated tools, though, maintaining these boundaries becomes tricky. Developers might inadvertently cross these boundaries, leading to issues with maintainability and scalability.

Creating Projects and Boundaries with Nx

Firstly, we must have projects and boundaries before we start enforcing boundaries restrictions. Nx simplifies this process with its generators that swiftly create applications and libraries. Here is a simple command to generate a library:

nx generate @nx/js:library feature-booking
Enter fullscreen mode Exit fullscreen mode

But a main ingredient is missing; Nx project tags are crucial here. They act as written definitions of our boundaries. For instance:

// project.json
{
...
  "tags": ["bounded-context:booking", "type:feature"],
...
}
Enter fullscreen mode Exit fullscreen mode

In the current example, we define a vertical slice or vertical layer based on the bounded context containing the library: The booking bounded context. Not only that, but we also define a horizontal layer based on the type of the library: The feature type.

A Rectangle containing a smaller square. The rectangle represents the bounded context, the square represents the library. The rectangle has a caption: Booking, and the square another: feature. Both captions make reference to the level of the tags.

Additionally, we can use grouping folders with Nx to further streamline the structure. I believe there should be a direct connection between tags and grouping folders. I've discussed these patterns extensively in my article "Semantic Grouping Folders with Nx."

Enforcing Project Boundaries with Nx

With our projects and boundaries in place, we can utilize the power of the Nx @nx/enforce-module-boundaries ESLint plugin to set restrictions between layers.

For example, if we want to restrict that, a library belonging to the booking bounded context can only import libraries from the same bounded context. Or in other words, if we want to enforce the vertical boundary defined by the bounded-context tags, we can do the following:

        "@nx/enforce-module-boundaries": [
          "error",
          {
            "enforceBuildableLibDependency": true,
            ...
            "depConstraints": [
              ...
              {
                "sourceTag": "bounded-context:booking",
                "onlyDependOnLibsWithTags": ["bounded-context:booking"]
              },
              ...
            ]
          }
        ]
Enter fullscreen mode Exit fullscreen mode

Image description

The same can be done for our horizontal layers. In the following example, we will enforce the following:

  • libraries with type feature can only import libraries with type feature, domain, ui
  • libraries with type ui can only depend on libraries with type ui and domain
  • libraries with type domain can only depend on libraries with type domain.
        "@nx/enforce-module-boundaries": [
          "error",
          {
            "enforceBuildableLibDependency": true,
            ...
            "depConstraints": [
              ...
              {
                "sourceTag": "type:feature",
                "onlyDependOnLibsWithTags": ["type:feature", "type:domain", "type:ui"]
              },
              {
                "sourceTag": "type:ui",
                "onlyDependOnLibsWithTags": ["type:domain", "type:ui"]
              },
              {
                "sourceTag": "type:domain",
                "onlyDependOnLibsWithTags": ["type:domain"]
              },
              ...
            ]
          }
        ]
Enter fullscreen mode Exit fullscreen mode

Image description

With these restrictions in place, if you try to import a library that breaks one of these enforced boundaries, you will get an ESLint error. Here is a hypothetical situation:

// File: libs/boundary/domain/types.ts
import { Button } from '@my-org/ui';
Enter fullscreen mode Exit fullscreen mode
error  A project tagged with "type:domain" can only depend on libs tagged with "type:domain"  @nx/enforce-module-boundaries
Enter fullscreen mode Exit fullscreen mode

This error message signifies that we've attempted to cross a boundary, thus helping us maintain the integrity of our architecture.

And this is not everything; we can even restrict external imports as described in the docs. I've found this. tool extremely useful when using technologies that look similar like React, Qwik and SolidJS; or even backend and frontend like Angular and NestJS.

Conclusion

Enforcing project boundaries and layers easily was a dream I thought was too much to ask.

Nx's @nx/enforce-module-boundaries ESLint plugin offers a powerful solution. The more we understand its potential and learn to utilize its power, the more we move towards creating robust, scalable, and maintainable software solutions. Nx is genuinely leading the way in the future of software architecture.

Top comments (2)

Collapse
 
rheckers profile image
R Heckers • Edited

Nice post! We are using @nx/enforce-module-boundaries in our company as well, it's a great way to enforce boundaries and make sure not everyone is importing everything willy nilly into their application and/or library.

(One small thing, the command the generate a library does not use @nrwl anymore but @nx after Nx 15, so it should be nx generate @nx/js:library feature-booking)

Collapse
 
naxodev profile image
Nacho Vazquez

Thanks for the kind comments. Nx is truly making it easier to protect us from ourselves.

You are entirely right about the command. The habit tricked me :)