After having used the Angular mono-repo for over a year, I would like to share some of the criticisms and concerns I have for it.
All of us are guilty of this. We read this really cool article, or watched a cool video, or watched our elder cousins perform a cool trick. We want to mimic. This is what is happening with the Angular Mono-Repo trend.
Separation of Concerns
My main concern is the lack of separation of concerns.
“[Separation of Concerns], even if not perfectly possible, is yet the only available technique for effective ordering of one’s thoughts, that I know of.”
Git history is useless
Git does not care about your individual apps. In a mono-repo, the history is a massive list of all changes… ever. Say you are working on “App A”. How does one revert to an older version of “App A”, when “App B”, “App C”, “App D”, “App F” and “App G” all pollute the history too?
The answer is you can’t, unless you pre-pend “[App-A]” on the front of every commit. But then that introduces more overhead, need for due-diligence and potential for human error.
Another way is to keep tagging your app, so you have “check-points” for your app, but then that reduces the effectiveness of the expressive git history we have.
It is very difficult to find the commit which broke your app you’re working on. Not only can it be a commit related to your app, it can also be a commit related to the /libs
, or any of the aforementioned apps.
My concern is the lack of separation of concerns.
Mono-repos are Leaky and Fragile
Making a change somewhere may affect the project you are on.
Common scenarios:
- You are on your branch, and then you merge
origin/master
into your branch… compilation error. Someone changed an API on their branch - You take one week break from holiday, pull fresh from
origin/master
. It compiles fine and passes unit tests, but a shared component looks strange on your app, but it looks fine on other apps. - A team mate updates a dependency in
package.json
, it works fine for him, doesn’t for you. - A team mate makes a breaking change in a shared component, unaware its also heavily depended on by another app.
- A battle tested component is used, but for one specific app, the look and feel needs to be different. You are left no choice but to write ugly override CSS in your
app
. - 3rd party dependency is updated and it breaks all apps. The only option is to big bang update.
Tooling becomes slow
As the code base grows, the slower the tooling becomes. Running 500+ tests over the 30 apps in the mono-repo every-time you make a check-in becomes a bottle-neck in your work. The obvious answer is to “buy a better computer and move on”, but in some situations, it isn’t all that simple. My team switched from Karma to Jest because Karma was becoming slow after our ~10th app. Now Jest is starting to become slow.
We are left to only run tests of the specific app we are each working on. This is dangerous because a change in the /libs
folder can affect all apps, so ideally all apps should be tested. This removes some of the benefits of the mono-repo, and introduces the potential for human error.
Low vision
You have finally finished your work and decide to make a pull-request. How can you be sure it won’t affect other apps? A naive developer will say that if all the unit tests pass on all projects, then it’s safe to go. But from experience, passing unit tests could be a potential red herring.
You are left with no choice but to test every app. This is fine when they’re aren’t as many apps, but when the app list grows, it becomes tedious and naturally people will become lazy and miss a few. How are the people reviewing your PR supposed to know it won’t affect other projects without them themselves also testing?
Single package.json
The package.json
file becomes a behemoth of scripts and dependencies. This is messy and hard to read and look at.
Dependency management is harder
Because there is only one package.json
, there is only one definitive list of all dependencies. You are only allowed one version of each dependency. What if one app wants an older version of a dependency, but you really want the latest and greatest? It’s not possible to have a single dependency with two versions. What if you want to update Angular which has a breaking change? You would need to big-bang update.
I have raised an issue about this on the Nx repo: https://github.com/nrwl/nx/issues/1089
Shared libs are idealistic
The purpose of the shared /libs
folder is so that every project can share components, increase re-usability and reduce repeated code. Hooray!
In theory this sounds like a no-brainer. But in practice, each app requires the DatePickerComponent
, the BarChartComponent
, DialogComponent
or the ItemDisplayComponent
to be slightly different.
Case Study: Dialog Component
Let’s say the DialogComponent
now requires an “Are you sure?” popup to come up when you press cancel for one of the apps, but you were told this is not needed in the other apps. You do what you would normally do, and add special cases inside the specific shared component. Maybe add a flag:
if (this.showAreYouSure) {
// Show "Are you sure?"
}
Afterwards, your team-mate working on a separate app who is also using the shared DialogComponent
was told by his boss to dim to background whenever a Dialog Opens. You don’t want to affect the other apps, so you again… add another hack/flag to make it working.
Then in another app, they want the close button to be a “X” the top-right rather than a “Close” button on the bottom right. Another hack/flag is added.
Another app may want a red border and drop-shadow on the modal. You then add ugly CSS overrides in your app including !important
tags to force it to look the way you want.
Overtime, these seemingly minor hacks build up, and turns into a shared monster.
You become scared to contribute to the shared components because it is relied by too many apps. You become too scared to refactor the component because it is so coupled with everything in the repository. You become scared to change the .html
and .css
because other apps have style overrides. You fear the code-base. It is tangled like vines.
The shared component gets too complex for anyone one single person to understand, which means even more hacks are added. It becomes un-readable, un-scalable and un-maintainable.
Shared /libs
is unrealistic, and potentially dangerous.
The quote below hits the nail on the head of why shared /libs
are flawed:
Separating concerns by files is as effective as separating school friendships by desks. Concerns are “separated” when there is no coupling: changing A wouldn’t break B. Increasing the distance without addressing the coupling only makes it easier to add bugs.
Increasing the distance (moving code from /apps
to /libs
), does indeed allow for re-usability and code sharing 👍, but I am not convinced if it warrants the added complexity and potential for bugs.
Summary
I believe the mono-repo definitely has it’s uses, but if over-used, it decreases productivity and is expensive.
Google made it work because they invested a lot of time into their tooling and workflow to prevent all of these issues. But for everyone else, it is simply not scalable; a house of cards waiting to collapse 💥.
Introducing the mono-repo shouldn’t be a simple decision or be directed by trends. Just because someone you admire uses the mono-repo doesn’t mean you have to as well. It should be part of an ongoing evolution with a lot of thought and research.
Thanks for reading! ❤️