Managing shared code in the Angular world — Monorepo vs Multirepo ⚔️

Vlodko1
8 min readFeb 9, 2021

I have been working with single-page applications since 2014 (check out my LinkedIn profile to see what I’m up to now), and day after day the complexity of front-end development has only been increasing. When I started to work with Angular 2 back in 2016 there was no decent solution for managing multiple apps, shared components, or feature modules. Luckily, today the situation is a lot better.

Recently, I have come to the point where the complexity of the project forced me to make a decision on how I want to manage the shared code and internal dependencies.

The Problem

I have been working on multiple single-page applications for the past few years. They all lived in separate git repositories and didn’t share any code. During the last few months amount of features has grown and a few more standalone apps are coming too. I made a decision to find the best way to organize the codebase, and also bring all front-end apps to a common denominator.

The next image shows the old state of things — basically, each repo contained a single Angular app. After a successful build of the main branch, an app was deployed to a particular environment.

These are the goals I wanted to achieve in the end:

  • Share components, modules, styles, assets, etc across multiple apps
  • Efficiently create and develop new applications along with reusable modules
  • Build, test and deploy apps in a fast and efficient way

Looking around

I’ve spent some time on the research and found these two most common ways of managing shared dependencies. Now, let’s look closer.

Monorepo — a set of apps and libs which could be run, built, and deployed independently, but still live in a single repository. The code is easily shared within the repo. Smart tooling that eliminates excessive usages of resources during builds, testing, etc is a must here. Please, do not confuse this monolith — one huge repo where we store everything. No particular tooling used, just a big ball of mud.

Multi-repo — the same set of apps and libs, but split into multiple git repositories. Each app usually lives in its own repo. Shared modules can be stored in one or many repos. NPM packages are used for sharing code across the apps.

Multiple repositories with NPM modules

Overall, creating reusable NPM packages has been a thing for a long time. And today it is still widely used within large organizations. We are not mentioning open-source, of course. Because open-source NPM modules are major building blocks for every front-end project. Luckily nowadays there are a lot of great tools for package management. If you are working with Angular consider using the ng-packagr for creating libraries. Multirepo approach brings a bunch of benefits as well as a few downsides:

Pros

  • Granularity — every code block has its own space, with tooling, 3rd party modules and etc.
  • A change in a shared repo doesn’t break dependents until you update its version.
  • Updating 3rd party dependencies in chunks — one repo at a time.
  • Continuous deployment configuration doesn’t require many changes.

Cons

  • Managing multiple repositories with package.json and other configuration files.
  • The development workflow is taking more time — working in multiple repositories when building a feature, creating multiple pull requests, updating versions of shared libs.
  • Maintaining a private NPM registry for storing shared modules.
  • Navigating through codebase is harder — need to have a repository tree in mind to understand the project structure.
  • Using the NPM/Yarn link for combining shared libs with an app during local development takes extra time.

Project structure in multirepo

Every app project lives in its own repo. All reusable modules could be located in a single repo. In the Angular world, this is easily managed with ng-packagr. It’s also possible to have a repo per module, but it has a big overhead in terms of supports multiple repositories, creating new repos for every new lib, and so on.

Development workflow

The process of developing new features and/or fixing bug might look the next way:

  1. Implement the change in a shared module and any of the apps.
  2. Link the module repo to the app to verify the changes (using NPM/Yarn link).
  3. Create a PR for a module repo — this should be merged first. After the merge, a new version is published to the NPM registry.
  4. Create a PR for an app repo. Update version of the shared module if needed. Sadly, sometimes developers forget to update the module versions in the app when a bug or feature was only related to a particular library.
  5. Deploy the updated app to a server.

Versioning of NPM modules

Having shared dependencies as NPM modules makes sense for open source projects and large enterprise companies with dozen of teams. There are many ways how to deal with versions for dev and prod deploys. Consider this approach:

For pull requests to the develop branch the version of a library is updated automatically. For example, you can use the existing module version + build number: 1.2.0–948785. Then in the app’s package.json, look for the last version available: "module1": "^1.2.0" and rebuild it.

Before release, merge a dev branch to the main branch for every shared library. This should publish a production version of the module to a production NPM registry — this registry will only contain module versions used for apps in production.

The topic of versioning is debatable and somewhat complicated, so give it try and see if it works for our organization.

Other ways to consider

I would highly recommend looking at the next projects: Rush from Microsoft:

Rush makes life easier for JavaScript developers who build and publish many packages from a common Git repo.

And open-source project Lerna:

Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.

This was a high-level overview of the multirepo approach, now let’s review monorepo way with help of Nx Workspace

Monorepo way

Monorepos have been in a wild for some time. But if you are not familiar with it, please, check this unopinionated overview.

Depending on the tech stack in your project, there might be some useful tools to work with monorepos. Since our front-end is built on the latest Angular framework, so we’ve decided to evaluate Nx — it is a tool, built on top of Angular CLI that extends some of Angular’s default functionality. It has been in the field for around 3 years and gathered a lot of professionals who helped to develop Angular and its ecosystem. Right now Nx also supports React, Next.js, as well as Express and Nest.js.

More info about Nx Workspace.

Monorepo and Nx Workspace has the following advantages:

  • Develop multiple projects in the same repository — Angular, React, Node.js back-end. Easily share interfaces between FE and BE projects. The projects depend on each other and share code.
  • No need for a private NPM registry, unlike a multirepo approach. No need to worry about versioning of shared libs in the case of NPM modules.
  • Working on a large feature is a lot simpler — a big feature is implemented in a single PR in a single feature branch. Easier to stay in context.
  • One package.json — easier to manage dependencies across multiple apps and libs. No more Angular upgrades in a dozen of repos.
  • Nx Affected — you don’t need to rebuild and retest every project in the monorepo during CI/CD workflow.

Of course, there are some downsides:

  • One package.json — in case of upgrading a 3rd party library then you must do it in a single attempt for all apps and libs. Install time during the build is longer since we are installing dependencies for all projects. A possible solution for this could be using cached node_modules.
  • Security limitations — today there is no way to limit specific developers from accessing folders within a single repository in Github.
  • A more complex continuous deployment configuration — creating a CI/CD pipeline might be a bit trickier for a monorepo than for a regular repo that contains a single project.

Project structure according to Nx default setup

Let imagine two apps — portal and admin panel. And also, an API baked using Nest.js

apps |
admin
admin-e2e
portal
portal-e2e
api
libs |
components
styles
assets
interfaces
...
etc

Monorepo workflow

A quick glance at a workflow using Nx workspace.

  1. Implement a feature in lib and/or app and create a single pull request.
  2. After the changes have been merged, the continuous deployment pipeline kicks in. Nx compares changes in a feature branch to the main branch and builds, tests only affected pieces.
  3. Deployment of only affected apps is performed

Misconceptions about Monorepos

Monorepo approach is highly debatable and not everyone fully understands this paradigm. Let’s clear a few common misconceptions. Find out more here.

Monorepo === Monolith — Nx allows us to deploy only the affected applications. We can still build and test only affected pieces. We don’t need to deploy all at once.

It lets other teams change my code without my knowing — GitHub has a feature called CODEOWNERS. It allows having developers responsible for certain parts of code even in a single repository.

It does not scale — when you reach 100k files or more you should probably move to other version control systems like the Virtual Filesystem for Git.

Builds are slower, every change will require a build of everything in the repo — Nx handles this by providing build/test/e2e affected scripts. Also, you can take advantage of Nx Cloud or local computation caching

Decision made

Photo by Roman Bürki on Unsplash

Taking into account the requirements I listed above and also projecting a future growth of the product suite, I have decided to take a monorepo way. I didn’t want to take the burden of maintaining a private NPM registry and managing module versions across multiple apps. And secondly, I feel like monorepo is a great choice for startup-like companies with rapid development. Of course, without Nx Workspace my decision could be different because it provides tools that eliminate some of the disadvantages of regular monorepo.

It’s been only a couple of months, but it does feel better and the development process is a lot smoother this way. In the process of migration to monorepo I have also improved our CI/CD workflow, stay tuned and we will share some great tips in the next post.

P.S.: If you have your own story on adopting a multirepo and/or monorepo approach, please, share it in the comment section below.

Resources:

Creating libraries — https://angular.io/guide/creating-libraries
NPM Link —
https://docs.npmjs.com/cli/link
Rush —
https://rushjs.io/
Lerna —
https://lerna.js.org/
Getting started with Nx —
https://nx.dev/latest/angular/getting-started/getting-started
Code Owner on Github —
https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
Git at Enterprise Scale —
https://github.com/microsoft/VFSForGit
Misconceptions about Monorepos —
https://blog.nrwl.io/misconceptions-about-monorepos-monorepo-monolith-df1250d4b03c

--

--