Nx - Module Boundary Strategy for Full Stack TypeScript Repositories

Do you love circular dependencies and a disorganized code base? Of course not, we want a great developer experience in our monorepositories that organize, enforce architecture, and prevent runtime errors.

Nx ships with a powerful feature to help developers navigate these large code bases, module boundaries. Module boundaries exist to separate projects and certain npm packages from being consumed through lint rules and project tagging.

So what should be considered when creating a module boundary strategy and what does Social Sprinkler's look like?

Before You Begin

It is assumed you have an understanding of Nx workspaces and tooling.

Goals

The Social Sprinkler web application exists in the workspace as a full stack NestJs and Angular app. The overarching goals is to describe the different project types that exist.

  1. Determine the Tech Stack and Library Types
  2. Define the Tags All Projects Will Inherit

Determine the Tech Stack and Project Types

Social Sprinkler is a full stack application built on top of several Google Cloud services and connects to multiple social networks. Angular and NestJs frameworks were chosen to ease the ability to share data transfer objects and utility functions between the frameworks. The storage tier consists of a MySql database for structured data and Google Cloud Storage for unstructured data. Finally, TailwindCss is used for the design system of the UI.

Knowing the tech stack now, what types of projects should exist to support the stack? How should I best segment the responsibilities of certain modules?

Segmenting by Domain

The domains are segmented from a technical / design system standpoint then business logic. Many implementations I’ve seen use a shared directory that any of the business domains can use. These are typically tagged with the format domain:<DOMAIN_NAME>.

The apps folder includes organization apps into separate folders. Social Sprinkler run seven different services each of those services belong to a domain of Social Sprinkler.

Shared exists as the foundation on which apps are built. Shared has it's own set of subdomains logically separated. For instance, a data-access domain contains further subdomains for unstructured and structured data. The Tailwind domain contains design system components and directives

Shared Directory Domains with Data-Access Domain with subdomain Unstructured and Structured

As developers compose their features, the shared domain supports Social Sprinkler's composition of Angular components and NestJs services. Because Social Sprinkler is a consumer of shared, a boundary is configured to only allow domain:shared to consume domain:shared. Social Sprinkler domains can consume anything within its own domain or shared.

Workspace Boundaries showing Shared as the base domain for Social Sprinkler and BEO

What boundaries exist in the Social Sprinkler Domain?

Drilling down further into Social Sprinkler a ‘core‘ subdomain supplies all other domain capabilities needed. A UserContextService lives in the ‘core’ domain to read JWT authorization claims.

The Media domain contains all the business logic for media creation, storage, and retrieval.

The User Management domain provides user functionality such as creation, forgot password, and organizational team assignments. Because users and organizations have image icons, it is a consumer of Media.

The Social domain consumes both media and user management. The social media channels use features and services of media and user management. Images and videos attached to a social media post are read from the media domain.

Publisher uses media, user management, and social domains. It's responsibility aggregates everything Social Sprinkler is doing for publishing posts to a social media channel.

Knowing the lay of the domains, what do these boundaries look like graphically?

Social Sprinkler's Core domain can be consumed by all other domains. Media

Framework Boundaries

While the repository is Typescript based to promote sharing code easily, I’ve witnessed bugs as Angular code leaks into NestJs code. The most problematic case was the inability for a Docker Container to start.

How do I prevent this in Social Sprinkler?

Each project includes a framework tag in the format of framework:<FRAMEWORK>. When a domain contains highly coupled full-stack code in it, the projects live close by to one another in a consistent directory structure.

Three framework types exist. any, ng, and nest.

The any tag consists of code that can be used in both the ng and nest applications. A good example of this is data transfer objects. No framework specific code exists in these projects. Within the aspect of a domain, a common directory contains framework:any tagged projects.

Angular specific projects include tag the framework:ng tag. Angular projects within a domain have a ui directory to contain its projects.

The api directory contains NestJs projects tagged with the framework:nest.

How are the folders organized under the domain?

Under the social directory, an api, ui, common, and analytics. The analytics folder is a subdomain of social containing the ui, api, and common directories.

Define the Library Types

So far boundaries centered around the tech stack and domains. The type tag relates to the framework. These boundaries help avoid circular dependencies and enforce a hierarchy. The type tag will be highly dependent on your chosen architecture.

The type tag used the type:<LIBRARY_TYPE> format.

Common Boundaries - Directory common

The common directory consists of models, data transfer objects, simple validations, interfaces and utilities. These files exported out common can be consumed by framework specific platforms. The types should refrain from using an platform specific types or APIs.

Two libraries exist in here, types and utilities. types may only consume other types libraries. utilities may consume types and other utility libraries.

Directory: types, Tag: type:types

The types libraries contain interfaces, classes, and types shared within the domain or with other domains higher in the domain hierarchy. The types also do not belong to a specific platform allowing them to be used in Angular or NestJs.

Directory: utilities, Tag: type:utilities

The utilities libraries also do not belong to a specific platform. They help give parity between the backend and frontend code. For instance, the media domain in Social Sprinkler formats the static assets URL. The utility needs to be used in both the browser and server.

Angular Boundaries - Directory ui

Framework specific libraries depend more on the chosen architecture. Social Sprinkler uses a hierarchical approach.

Directory: <DESIGN_SYSTEM_NAME>, Tag: design

The design project contains directives and components for composition of larger smart components. They stand as building blocks. The components are akin to atom or molecules for Atomic Design. For instance, my repository has a Tailwind set of components and another library contains SVG icon components.

Directory: data-access, Tag: data-access

The data-access projects contains services for backend communication and state management. If using NgRx, the effects, actions, selectors, reducers belong here. This goes along with other patterns such as service with a subject.

Directory feature-<FEATURE_NAME>, Tag: feature

feature projects tie design and data-access libraries together. Generally, components responsible for reading and mutating organize into features. They are meant to allow the user to accomplish a particular task. The templates are a composition of the design project components, and are driven by services from the data-access projects.

Others in the Angular community include routing into feature libraries. Including routing can complicate reusing certain features in different parts of the application. Social Sprinkler contains a media-feature which contains components for handling image / video uploads and display.

Directory experience-<EXPERIENCE_NAME>, Tag: experience

experience projects can consume design, feature, data-access, and other experience projects. Routing configuration, template components, and page components exist in this project.

The primary goal of experience projects is configuring a workflow based on routes. Social Sprinkler's domain user-management-social ties a customer's organization together with different social media experience projects.

Directory <APP_NAME>, Tag: app

app composes all the experience projects into a deployable / shippable unit. It sits a the top of the hierarchy.

NestJs Boundaries - Directory api

Using Onion Architecture, the api directory breaks down into domain, data-access, services, and application. The inner layers know nothing about the outer.

The domain library focuses on entities specific to the database, service contracts, and configuration.

This is the core layer of the api platform. This layer knows of only the types and utilities projects.

Directory data-access, tag: data-access

The data-access library communicates with data storage mechanisms. This could be MySql, GCP’s Storage buckets, or reading writing to disk.

Most of the services with the data-access follow a repository pattern which are responsible for CRUD operations in a MySQL DB.

Directory services, tag: services

Services contain logic for the business domain. It consumes other services, data-access repositories, domain, and common libraries.

Directory: application, tag: api

The api project contains controllers and the bundled module that will roll up into a deployable app. It consumes the NestJs services and domain projects.

Directory: <APP_NAME>, tag: app

app composes one or multiple application libraries together for a deployable unit of code.

Summary

Module boundaries enforce Social Sprinkler’s architecture.

Splitting technical / design projects in a shared domain helps reuse code for future projects. Both BEO and Social Sprinkler use common design components allowing for easier updates and smart component composition. This is where domain boundaries help.

Full stack workspaces using the same language as TypeScript could allow server side and browser code to mix. Runtime errors occur when the execution context doesn’t support the global apis. But we do want to share certain models and utilities. So I enforce this with framework boundaries.

Finally, Social Sprinkler’s architecture contains many different project types for the supported frameworks. Tagging the projects with a particular type tag can help determine types of files generated and which project they live. This will be dependent on the architecture / folder organization you want to enforce.