The Monorepo Blueprint – How to Create a Scalable Architecture for an Angular Monorepo

Share on facebook
Share on google
Share on twitter
Share on linkedin

This article will teach you how to create a scalable architecture for an Angular monorepo. The principles here can be applied to any front end monorepo, as they are based on universal best practices for getting a scalable and maintainable architecture.

But first, what should a good Angular architecture do?

  1. Separate UI from business logic using abstraction layers
  2. UI should be decoupled from technology specific tools such as communication and state management
  3. The architecture should reflect the domain knowledge, not the technologies
  4. The architecture should enforce module boundaries to ensure proper encapsulation and dependency control

Let’s consider how this will look like for a todo app that:

  • Have a web and mobile app
  • Can show todo list
  • Can show a todo list admin dashboard
  • Can get and save todos using a todo service (node app)

 

 

The overall setup can be created using Angular and NX (for affected commands and TsLint rules for module boundaries).

Let’s look at how all of this ties together.

Apps

Apps here should only focus on the UI and should delegate all data access to the corresponding sandbox. That means that there should be no HttpClient or state management framework referenced inside of an app. This gives us the luxury of having apps decoupled from the data access logic, which makes it easier to later change the state management framework or communication technology.

Libs

Normally you might think of libraries as a means to code sharing. That is partly true here, but it can also have other purposes: enforce encapsulation of modules. By creating a library we can use the Nx TsLint rules to enforce that libs can only be communicated with through the public interface (normally a barrel export index.ts file) which helps with a more maintainable architecture because of better encapsulation and control of the dependencies. It will also break your app up in smaller independent pieces, which will make troubleshooting and test execution faster. For each lib, you can freely choose eg. the testing framework and build configuration without it is affecting other apps’ setup (it might still be a good idea to keep the app consistent).

App-specific libs

We want to create a library specific to each app to encapsulate all the data access logic. We do this by only exposing “sandboxes”.

Sandboxes

A sandbox is a facade which can contain business logic. It is used for setting and getting data through an easy-to-use interface. Why is it easy to use? The interface of a sandbox should never contain technology-specific terms such as: dispatch and selector. You will just write methods with a clear intent in the domain language eg. saveTodo.

Notice on the picture above that there are no effects/thunk/epic/saga. That is because it is handled in the sandbox. Why don’t I use effects?

  1. Complicates the interaction with indirection when there is no one to many communication
  2. Stream dies if an effect throws an unhandled error, resulting in a broken app and need for defensive error handling in all places
  3. Couples business logic to a framework specific technology – harder to change
  4. A state management framework is for state management, not business logic (IMO!)

This is my personal opinion and experiences from seeing these problems over and over in teams, and simply fixing them by not using effects/epics and such. Also, one big drawback with effects is that you need to write protective error handling to make sure you keep the stream alive and don’t break the app, which complicates things quite a lot

Notice here how the sandbox is the externally exposed interface, which will handle business logic and the delegation to the state management framework and HttpClient. By having this abstraction in our architecture we can easily change the state management and communication framework without it is affecting the apps. This is the power of good abstractions and encapsulation. From the app developers perspective, he might not know anything about NgRx and HttpClient, but because the abstraction is so easy to use, he doesn’t need to if he is only working with UI.

Because an app lib should only be used by that specific app it is a good idea to enforce this constraint by tagging the app and the lib with the same tag, eg. todo-app and enforce that only todo-app lib can be called by todo app using Nx TsLint rules.

Shared

Inside here goes the libs that can be used by multiple apps. You should enforce the module boundary by using tagging and setting the Nx TsLint rules accordingly.

Grouping of shared libs

A shared lib can be grouped after:

  1. Feature
  2. Platform (web, iOS, Android)
  3. Function (UI, data access)

Feature

A feature library is a grouping based on a specific feature in the app. That could eg. be the responsibility for authentication. Here you might have the login page (smart component) which will contain the necessary UI and data-access logic to log users in and out.

Constraints:

Feature libs can be used by apps only.

UI

The UI lib should only contain shared presentation components. That means no feature context should go here. All interaction is through input and output. You want to copy UI component from apps to here whenever a UI component should be shared with the other apps.

Constraints:

UI libs can be used by apps only.

Data Access

Here we have the shared data access and HTTP logic, that can be used by sandboxes. That includes HttpInterceptors, shared reducers, actions, and selectors for example.

Constraints:

Shared data-access libs can be used by all projects except UI and utils libs.

Utils

Shared utils libs are normally containing static pure functions, that provide different kinds of helpers. This is for example date utils.

Constraints:

Utils can be used by all projects.

Conclusion

In this post, we saw how to create a scalable monorepo architecture that will ensure maintainability by:

  • Decoupling UI and business logic
  • Ensure dependency control and module boundaries
  • Domain-based instead of technology-focused

By having this architecture in place it is possible to change eg. state management framework inside of the sandbox without needing to change the apps, because of the decoupling and the abstraction using sandboxes. Also, the module boundaries help the team to make sure the architecture stays clean.

Coming up

I will go through how to set this up completely in the next article on a code level. Make sure you subscribe so you don’t miss it.

References

https://blog.strongbrew.io/A-scalable-angular2-architecture/

NRWL book Angular enterprise monorepo patterns

Do you want to become an Angular architect? Check out Angular Architect Accelerator.

Related Posts and Comments

How to Set up a CI pipeline with Azure Pipelines and Nx

It goes without saying that having a CI pipeline for your Angular apps is a must. Setting one up for regular Angular apps is fairly straightforward but when you have an Nx monorepo there are certain other challenges that you have to overcome to successfully orchestrate a “build once, deploy many” pipeline. This post will

Read More »

How to Set Up Git Hooks in an Nx Repo

Git hooks can be used to automate tasks in your development workflow. The earlier a bug is discovered, the cheaper it is to fix (and the less impact it has). Therefore it can be helpful to run tasks such as linting, formatting, and tests when you are e.g. committing and pushing your code, so any

Read More »

The Stages of an Angular Architecture with Nx

Long gone are the times when the frontend was just a dumb static website. Frontend apps have gotten increasingly complex since the rise of single-page application frameworks like Angular. It comes with the price of increased complexity and the ever-changing frontend landscape requires you to have an architecture that allows you to scale and adapt

Read More »

The Best Way to Use Signals in Angular Apps

Since Angular 16, Angular now has experimental support for signals and there is a lot of confusion in the community about whether this is going to replace RxJS or how it should be used in an app in combination with RxJS. This blog post sheds some light on what I think is the best way

Read More »

High ROI Testing with Cypress Component Testing

Testing is one of the most struggled topics in Angular development and many developers are either giving up testing altogether or applying inefficient testing practices consuming all their precious time while giving few results in return. This blog post will change all this as we will cover how I overcame these struggles the hard way

Read More »