Skip to main content

Organize Libraries

Now that you've decided to divide your apps into libraries, you might have a few questions in mind:

  • How should we organize these libraries?
  • How should we categorize them?
  • How should we name them?
  • How granular should they be?

While there is no one-size-fits-all answer to these questions, here is some guidance to help you make informed decisions.

Library Categorization

You are free to organize your libraries in any way that makes sense to you, your team, and your organization. However, it is essential to define some rules to avoid ending up with a mess of libraries that are hard to understand and maintain. In addition, this will allow us to leverage certain Nx features that will enforce these rules and assist everyone on the team in following them.

Tags and Categories

Nx allows us to tag our applications and libraries with custom tags.

These tags can be provided when the application or library is generated using the --tags option:

nx g lib my-lib --tags=my-category

or afterwards by updating the tags property in the project.json file:

project.json
{
"name": "my-lib",
"tags": ["my-category"],
...
}

Tags can be used to categorize libraries based on different criteria. They are useful for:

These tags can be used to categorize libraries on different dimensions.

Type Categories Dimension

The first dimension of categories that can help you organize your libraries across various architectural styles is the type dimension.

This group is commonly used to define horizontal layers, or in other words, to segregate the technical responsibility of the library (e.g., container components vs. presentational components for the frontend, or controllers vs. repositories for the backend). Some common type categories are feature, ui, data-access, or infra. These are just examples that will be elaborated on below.

note

The naming convention in the Nx community is to prefix the tags with type: (e.g. type:ui).

Scope Categories Dimension

The second most common dimension of categories that can help you organize your libraries is the scope dimension.

It represents the vertical slices of the workspace. Essentially, it segregates the functional responsibilities of applications and libraries.

For example, given a recipe catalog application, you could have the following scopes: auth, catalog, and cart.

If you are familiar with the Bounded Context of Domain-Driven Design (cf. https://martinfowler.com/bliki/BoundedContext.html or https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215), you can view the scope dimension as a tactical approach to defining the boundaries of a bounded context.

If you're unfamiliar with this concept, consider it as a way to define the scope of a specific set of functional responsibilities. It establishes the boundaries within which certain concepts, terms, and business rules apply, allowing you to focus on the task at hand without distractions from other parts of the workspace. More precisely, a bounded context defines the boundaries where a particular model is applicable.

Other Dimensions

In larger workspaces, you might want to define additional categories to help you organize your applications and libraries.

For instance, the platform dimension is commonly used to define the platform on which the application or library is intended to run: mobile, server, web, crossplatform, etc...

In even larger workspaces and organizations, you might want to define the department or team dimension to identify the department or team that owns the application or library: sales, marketing, finance, etc.

Library Type Categorization Examples

As mentioned above, you are free to organize your libraries in any way that makes sense to you. However, determining or agreeing on the categories to use, especially for the type dimension, can be challenging. To help you get started, here are some examples:

Light Layered Architecture

For the simplest workspaces and for small teams that can properly apply separation of concerns without enforcing any boundaries, you might want to consider a simple layered architecture like this one. However, remember that merging libraries is often easier than splitting them.

TypeDescriptionContent
appAn application.Frontend:
✅ App configuration.
Backend:
✅ App configuration.
✅ Controllers.
✅ Serverless handlers.
featureFeature-specific logic.Frontend:
✅ Page components.
✅ Container components.
✅ Facades.
✅ Services.
✅ State management, stores, and effects.
🛑 Almost no styling except for some layout.
Backend:
✅ Use cases.
✅ Services.
uiAbstraction layer of the UI.Frontend:
✅ Presentational (a.k.a. dumb) components.
✅ UI services (e.g. Dialog).
Backend: -
infraAbstraction layer of infrastructure concerns.Frontend:
✅ Repositories or remote service adapters (e.g. HTTP, or GraphQL clients).
✅ Non-UI browser API adapters (e.g. Speech Recognition)
Backend:
✅ Remote service adapters (e.g. HTTP, or GraphQL clients).
✅ Repositories or database adapters.

Pros and Cons

  • ✅ This architecture style is straightforward and easy to understand.
  • ❌ Without caution, upper layers tend to get contaminated with infrastructure concerns. (e.g. Remote service types (DTOs) can be propagated to the feature layer.)
note

With this architecture, you will quickly notice the need for a model layer to share the domain model between the feature, infra, and ui layers.

Hexagonal-Inspired Architecture

For the most complex workspaces or for large teams who want to enforce strict separation of concerns, you might want to consider a hexagonal-inspired architecture similar to this one.

TypeDescriptionContent
appAn application.Frontend:
✅ App configuration.
Backend:
✅ App configuration.
✅ Controllers.
✅ Serverless handlers.
featureFeature-specific logic.Frontend:
✅ Page components.
✅ Container components.
🛑 Almost no styling except for some layout.
Backend:
✅ Use cases or feature-specific services.
domainReusable business logic.Frontend:
✅ Facades.
✅ Reusable services.
✅ State management, stores, and effects.
✅ Ports injection tokens.
Backend:
✅ Reusable services.
✅ Ports injection tokens.
modelThe model applicable inside a given bounded-context (cf. Scope Dimension).Frontend:
✅ Entities generally formed by the combination of interfaces/types/enums/functions.
🛑 Almost no external dependencies.
🛑 Framework-agnostic code only.
Backend:
✅ Entities: classes or interfaces & functions.
🛑 Almost no external dependencies.
🛑 Framework-agnostic code only.
portsInfrastructure abstraction.✅ Ports: interfaces abstracting the infrastructure.
🛑 Almost no external dependencies.
🛑 Framework-agnostic code only.
uiAbstraction layer of the UI.Frontend:
✅ Presentational (a.k.a. dumb) components.
✅ UI services (e.g. Dialog).
Backend: -
infraInfrastructure implementation.Frontend:
✅ Adapters implementing ports.
✅ Repositories or remote service adapters (e.g. HTTP, or GraphQL clients).
✅ Non-UI browser API adapters (e.g. Speech Recognition).
Backend:
✅ Adapters implementing ports.
✅ Remote service adapters (e.g. HTTP, or GraphQL clients).
✅ Repositories or database adapters.

Pros and Cons

  • ✅ This architecture style enforces strict separation of concerns.
  • ✅ It prevents the contamination of the application's core with infrastructure concerns. (e.g. changes to remote services have less impact and require less refactoring/restructuring.)
  • ✅ It can simplify testing.
  • ✅ It can speed up some tests by not loading the infra category and its dependencies, and also thanks to Nx graph, Nx will not rerun domain, feature or core tests when infra category is the only one that changed.
  • ❌ It is more complex and might be overkill for small teams or simple applications.
  • ❌ Dependency inversion can defeat the purpose of tree-shakability (i.e., services must be provided when the app or feature is loaded). This can lead to larger bundles, potentially harming performance for both frontend and backend applications. Indeed, providing unnecessary infrastructure services in the backend can result in slower deployment and more significant cold starts.
  • ❌ The dependency inversion requires more boilerplate in this case. (i.e. injection token + interface + implementation.)
tip

ports can be merged with model if you want to keep the number of libraries to a minimum.

note about injection tokens location

Note that injection tokens are in the domain category to prevent implementations in infra from injecting other ports by mistake. This also applies to ui if ports and model are merged: if the tokens are in model, ui could inject infrastructure services.

Modular Layered Architecture

The example below is a layered architecture with fine-grained horizontal slices that emphasize both separation of concerns and simplicity. It is a balance between the two previous examples.

TypeDescriptionContent
appAn application.Frontend:
✅ App configuration.
Backend:
✅ App configuration.
✅ Controllers.
✅ Serverless handlers.
featureFeature-specific logic.Frontend:
✅ Page components.
✅ Container components.
🛑 Almost no styling except for some layout.
Backend:
✅ Use cases or feature-specific services.
domainReusable business logic.Frontend:
✅ Facades.
✅ Reusable services.
✅ State management, stores, and effects.
Backend:
✅ Reusable services.
modelThe model applicable inside a given bounded-context (cf. Scope Dimension).Frontend:
✅ Entities generally formed by the combination of interfaces/types/enums/functions.
🛑 Almost no external dependencies.
🛑 Framework-agnostic code only.
Backend:
✅ Entities: classes or interfaces & functions.
🛑 Almost no external dependencies.
🛑 Framework-agnostic code only.
uiAbstraction layer of the UI.Frontend:
✅ Presentational (a.k.a. dumb) components.
✅ UI services (e.g. Dialog).
Backend: -
infraInfrastructure implementation.Frontend:
✅ Adapters implementing ports.
✅ Repositories or remote service adapters (e.g. HTTP, or GraphQL clients).
✅ Non-UI browser API adapters (e.g. Speech Recognition).
Backend:
✅ Adapters implementing ports.
✅ Remote service adapters (e.g. HTTP, or GraphQL clients).
✅ Repositories or database adapters.

Pros and Cons

  • ✅ This architecture style plays well with tree-shakability. Infrastructure services do not have to be provided explicitly, they can be implicitly provided when used. (e.g. Angular's providedIn: 'root', or React's context's default value.)
  • ❌ It can't easily enforce that upper layers are not contaminated by infrastructure concerns. (i.e. Remote service types (DTOs) can be propagated to the domain or feature layer.)
tip

By implementing infrastructure service interfaces in the model layer, you will reduce the risk of contaminating the domain and feature layers by infrastructure concerns without adding too much complexity and without losing tree-shakability.

In other words, as model is not allowed to import types from infra so the interfaces it defines will be infrastructure-agnostic.

Choosing the Right Granularity

Deciding on the right granularity for your libraries is crucial: if they're too big, you lose some of the benefits of splitting your apps into libraries; if they're too granular, you might introduce unnecessary complexity.

Too Big

Creating excessively large libraries could diminish some of the benefits of splitting apps into libraries:

  • It could result in a monolithic library that is challenging to maintain and understand.
  • It might not fully leverage Nx's caching and parallelization capabilities.
  • Progressive migration could become more difficult (e.g., transitioning from Jest to Vitest, or changing lint or build options).

Too Granular

On the other hand, creating excessively granular libraries might introduce unnecessary complexity:

  • It could increase the cognitive load for developers who might struggle to understand the purpose of each library.
  • It will require more boilerplate as each new symbol must be re-exported by the library's public API (i.e., index.ts) before being used in other apps and libraries.
  • It might defeat the purpose of parallelization by over-parallelizing.
  • Without enforcing the boundaries, it could lead to highly coupled libraries or libraries that export implementation details.

The Right Size

Before deciding on the granularity of your libraries, here are some important factors to consider:

  • Workspace Ambitions: What are the goals of your workspace? Is it a small isolated application that is not meant to last, or is it a long-term product that will evolve over time? Are you planning to merge other repositories into this workspace? For instance, in the extreme case where one or two developers are building a small isolated application that is not meant to last, you might not need to split it into libs.

  • Team's Experience: How experienced is your team with the technologies and architectural styles you are using? Paradoxically, if the separation of concerns is not natural to your team, creating many libraries with clear and enforced boundaries will help them understand and apply the architecture better.

note

While you can always start with relatively large libraries and gradually split them into smaller ones, note that it is generally easier to merge libraries than to split them. 😉

Defining your Architecture

Before choosing an architectural style and the corresponding categories, consider the following:

  • Make sure to involve your team in the decision-making process.
  • Avoid dogmatism: the best architecture is the one that fits your team, your workspace, and your organization.
  • You are free to mix and match, but make sure that you have a clear understanding of the trade-offs (see the pros and cons of each example above).
  • Listen to the signals:
    • If you notice that some libraries are growing too large, consider splitting them into smaller ones.
    • If you notice the proliferation of passthrough libraries (a.k.a. Sinkhole Anti-Pattern), then you might want to simplify your architecture.
    • If your team struggles to understand the architecture, then you might want to simplify it.
    • If your team struggles to maintain a clear separation of concerns, then you might want to try a more restrictive architecture.
Mind the Sinkhole!

In order to avoid what is often referred to as the "Sinkhole Anti-Pattern", note that you do not have to always implement all the type categories. For instance, a really simple application without much business logic might only need the app, ui, and infra categories.

Also, in the same workspace, some scope slices might need less layers than others.

File Structure

You are free to organize the file structure as you see fit. However, the journey will be much smoother if anyone on the team can identify the categories of each library without having to check its tags.

For instance, making the scope, type, and eventually the platform categories appear in the library path is a common practice (e.g. libs/{scope}/{type} and libs/{scope}/{name}-{type}):

└── libs
├── cart
│ ├── feature
│ └── infra
└── catalog
├── infra
├── model
├── search-feature
├── search-ui
└── special-offers-feature

Note that, in contrast to a flatter approach (e.g., libs/{scope}-{type} and libs/{scope}-{name}-{type}), nesting allows the Nx graph to group libraries that are in the same folder.

Nx Graph Grouping

tip

You can always move the libraries around later using the move generator:

nx g @nx/workspace:move --projectName <name> --destination <new-path>

Your new friend, Nx, will take care of everything for you.

Additional resources