πŸ“¦ How to correctly manage dependencies in a yarn workspace (#74)

January 23, 2023

There are two technically correct ways to manage dependencies in a yarn workspace. I want to illustrate why one way is clearly better than the other.

Let’s say that you have a monorepo that’s structured in the following way:

β”œβ”€β”€ monorepo β”‚ β”œβ”€β”€ packages β”‚ β”‚ β”œβ”€β”€ package-a β”‚ β”‚ β”‚ β”œβ”€β”€ package.json β”‚ β”‚ β”œβ”€β”€ package-b β”‚ β”‚ β”‚ β”œβ”€β”€ package.json β”œβ”€β”€ yarn.lock β”œβ”€β”€ node_modules └── package.json

Imagine that both package-a and package-b are using TypeScript.

The first option is to define all of your shared dependencies in the root package.json file (monorepo/package.json), and all other dependencies in each package's respective package.json file. In this scenario, you would define TypeScript in monorepo/package.json.

The second option is to define all of your dependencies in each package's respective package.json file, even if those dependencies are shared. In this scenario, you would add TypeScript to both monorepo/packages/package-a/package.json and monorepo/packages/package-b/package.json.

This may sound counterintuitive, but option #2 is the better and more correct option in the long term.

Let's illustrate why option #2 is better by fast-forwarding this monorepo example by a couple of years. Let's imagine that your monorepo has expanded to include many more packages and apps:

β”œβ”€β”€ monorepo β”‚ β”œβ”€β”€ packages β”‚ β”‚ β”œβ”€β”€ package-a β”‚ β”‚ β”‚ β”œβ”€β”€ package.json β”‚ β”‚ β”œβ”€β”€ package-b β”‚ β”‚ β”‚ β”œβ”€β”€ package.json β”‚ β”œβ”€β”€ apps β”‚ β”‚ β”œβ”€β”€ app-a β”‚ β”‚ β”‚ β”œβ”€β”€ package.json β”‚ β”‚ β”œβ”€β”€ app-b β”‚ β”‚ β”‚ β”œβ”€β”€ package.json β”œβ”€β”€ yarn.lock β”œβ”€β”€ node_modules └── package.json

This example is much more realistic for what a monorepo looks like at a relatively mature company.

In this scenario, all of the apps are written using React. package-a and package-b contain shared React components that are referenced by all three apps. All of the code in the monorepo is on React 17.

One day, the engineering organization decides it's time to upgrade to React 18. If you had React 17 defined in the root package.json, you'd need to upgrade every single project all at once. There are a few reasons why this is undesirable:

  1. You can't parallelize the work.

  2. You can't test the upgrade path on lower-risk projects.

  3. Your team has to review and QA a massive chunk of work, rather than multiple incremental chunks.

On the other hand, if each package and app manages its own dependencies you can upgrade React much more strategically.

  1. Each team can slot in the upgrade where it makes sense in their roadmap.

  2. Different teams can work on the upgrade simultaneously.

  3. You can improve the quality of QA and reduce the potential for regressions by upgrading incrementally.

  4. If you do introduce a regression, you can rollback only the small area of the codebase that introduced it, rather than having to revert the entire upgrade for the whole codebase.


Talk soon,

Mae

Do you want design systems tips and tricks sent to your inbox?