
Module Boundaries
Rules for structuring your code


Daniel Worthy
Software Engineer on Match
- Been with JBH since Fall 2023
- Been building web apps 'professionally' since 2007
- I like to build stuff -> Wood, 3D printing, LEGO
- I try to watch every Godzilla movie every year
- I'm a cat person... now
What we are going to cover
- Define Module Boundaries
- High Level Benefits
- Boundary Types
- Tooling
Have you ever wondered where a new piece of functionality should live or where functionality currently resides?
What are Module Boundaries?
- Rules for structuring your code
- Define how different parts of your application interact
- Enforced by ESLint or by npm script
Why are Module Boundaries Important?
- Enhance code readability and maintainability
- Facilitate easier testing and debugging
- Promote reusability of code components
Applying Module Boundaries
{
"name": "my-app-admin",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/my-app/feature-admin/src",
"prefix": "my-app",
"tags": ["type:feature", "scope:my-app", "platform:web"],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/libs/my-app/feature-admin"],
"options": {
"jestConfig": "libs/my-app/feature-admin/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
Boundary Types as defined by NX
- app
- Servable application, e.g. a web app, mobile app, etc.
- feature
- Business logic, container/smart components
- data-access
- API access & state management
- ui
- Presentation components
- utils
- Pure code functions, formatters, DTO converters
What can use what?
- app
- can import all the things
- feature
- feature, data-access, ui, utils
- data-access
- data-access, utils
What can use what?
Replace _ with -. Hyphens don't work well in Mermaid Charts
Custom Types as defined by you
- models
- interfaces, types and enums
- e2e-utils
- reusable cypress commands for end-to-end and component testing
- assets
- Static assets, e.g. images, fonts, etc.
- store
- NGRX state management
How do we organize this mess?
Grouping Folders
app/
application
libs/
admin/ <-- Grouping Folder
data-access-admin
feature-admin
ui-admin
utils-admin
roster/ <-- Grouping Folder
data-access-admin
feature-roster
ui-roster
utils-roster
Nesting Folders
app/
application
libs/
admin/
data-access/ <-- Heavy Nesting
admin
feature/
details
profile
shell
ui/
admin
utils/
admin
Finding Balance
Heavy Libraries
libs/
admin/
data-access-admin <--Library
feature-admin <--Library
profile
user-import
user-list
ui-admin <--Library
avatar
list-item
utils-admin <--Library
helpers
Library Heavy
libs/
admin/
data-access <-- Everything is a library
feature-profile
feature-shell
feature-user-import
feature-user-list
ui-avatar
ui-list-item
utils-helpers
Quick Benefit
How types help build pipeline
Everyone Stays In Their Lane With Scopes
Let's Add Some Shared Stuff
Scopes Specificity
Small Apps
- scope:admin
- scope:roster
- scope:shared
Monorepo
- scope:application1
- scope:application2
Specific
- scope:application1:feature1
- scope:application2:feature2
You can write code to automatically manage this.
Other Types of Boundaries
- platform:mobile|web|desktop
- framework:angular|react|typescript
- component:core|bespoke
- version:modern|legacy
- developer:daniel|jacob
Quick Tip
Use NX Generators to automatically add module boundaries to new libraries.
Example
const projectName = options.name.replace(
const rootComponentName = options.name.slice(options.name.lastIndexOf('/') + 1);
const componentName = names(rootComponentName);
const removeUnitTests = options.libType === 'model';
const projectRoot = `libs/${options.scopeTarget}/${options.name}`;
const importPath = `@app/${options.scopeTarget}/${options.name}`;
const ngOptions = {
prefix: 'app',
name: `${options.scopeTarget}-${projectName}`,
directory: projectRoot,
importPath,
tags: `type:${options.libType}, scope:${options.scopeTarget}`,
skipModule: true,
unitTestRunner: removeUnitTests ? UnitTestRunner.None : UnitTestRunner.Jest,
standalone: true,
strict: true,
flat: true
};
await angularLibraryGenerator(tree, ngOptions);
Enforce Module Boundaries
"rules": {
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
enforceBuildableLibs: true,
depConstraints: [
{
sourceTag: "type:ui",
onlyDependOnLibsWithTags: ["type:ui", "type:utils"]
},
{
sourceTag: "type:feature",
onlyDependOnLibsWithTags: ["type:feature", "type:ui", "type:data-access", "type:utils"]
},
{
sourceTag: "type:data-access",
onlyDependOnLibsWithTags: ["type:data-access", "type:utils"]
}
]
}
]
}
Enforce Module Boundaries
{
"rules": {
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
enforceBuildableLibs: true,
depConstraints: [
{
sourceTag: "scope:application1",
onlyDependOnLibsWithTags: ["scope:application1", "scope:shared"]
},
{
sourceTag: "scope:application2",
onlyDependOnLibsWithTags: ["scope:application2", "scope:shared"]
},
{
sourceTag: "scope:shared",
onlyDependOnLibsWithTags: ["scope:shared"]
},
]
}
]
}
}
Additional Settings
{
"rules": {
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
enforceBuildableLibs: true,
allow: ['@shared/**', '@features/**', '@store/**', '@services/**'],
depConstraints: [
{
sourceTag: "scope:shared",
notDependOnLibsWithTags: ["scope:application1", "scope:application2"]
},
{
allSourceTags: ["scope:admin", "type:ui"],
onlyDependOnLibsWithTags: ["type:util"],
allowedExternalImports: ["@angular/core", "@angular/common"]
bannedExternalImports: ["*router*", "@angular/common/http"]
}
]
}
]
}
}
Migrating to Module Boundaries
- Identify a pattern you want to follow with your team
- Setup ignore for old folders/paths as you migrate
- Write a generator/executor to find libs without tags
- Leave code better than you found it
Migration Step 1
apps/
admin/
components/
services/
store/
admin.component
roster/
apps/
roster/
libs/
admin/
feature-admin/ <-- Feature Lib
components/
services/
store/
admin.component
Migration Step 2
apps/
roster/
libs/
admin/
feature-admin/
components
services
store
admin.component
apps/
roster/
libs/
admin/
data-access-admin/ <-- previously services and store
feature-admin/
admin.component
ui-admin/ <-- Previously components
Vanilla Angular Module Boundaries
They don't exist, but we can use a tool to enforce them
Example Sheriff Config
import {noDependencies, sameTag, SheriffConfig } from '@softarc/sheriff-core';
export const sheriffConfig: SheriffConfig = {
modules: {
'src/app/admin/feature': ['scope:admin', 'type:feature'],
'src/app/admin/data': ['scope:admin', 'type:data'],
'src/app/roster/feature': ['scope:roster', 'type:feature'],
'src/app/roster/data': ['scope:roster', 'type:data'],
'src/app/<scope>/<type>': ['scope:<scope>', 'type:<type>']
},
depRules: {
root: ['*'],
'scope:*': [sameTag, 'scope:shared'],
'type:feature': ['type:ui', 'type:data', 'type:util'],
'type:ui': ['type:data', 'type:util'],
'type:data': ['type:util'],
'type:util': noDependencies,
},
};
Sheriff ESLint Config
const sheriff = require('@softarc/eslint-plugin-sheriff');
module.exports = tseslint.config(
{
files: ['**/*.ts'],
extends: [sheriff.configs.all],
},
);
Sheriff CLI
npx sheriff init // Creates your sheriff.config.ts
npx sheriff verify main.ts // Verifies your module boundaries
Review
- Module boundaries help us get and stay organized
- Help developers find what they are looking for
- Should be defined and modified by the team

Book Recommendation
Tidy First

Contact

Links
Presentation Framework -Reveal.js