Skip to content

Commit

Permalink
docs(core): bring dependency management docs up to date with reality (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesHenry authored Feb 20, 2025
1 parent a94bc76 commit 2a82009
Showing 1 changed file with 43 additions and 8 deletions.
51 changes: 43 additions & 8 deletions docs/shared/concepts/decisions/dependency-management.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,56 @@
# Dependency Management Strategies

Where do you define the dependencies of your projects? Do you have a single `package.json` file at the root or a separate `package.json` for each project? What are the pros and cons of each approach?
When working with a monorepo, one of the key architectural decisions is how to manage dependencies across your projects. This document outlines two main strategies and helps you choose the right approach for your team.

The core decision comes down to:

1. Independently maintained dependencies in individual projects
2. A "single version policy", where dependencies are defined once at the root for your entire monorepo

**Nx fully supports both strategies - it's your choice and you can change your approach as your needs evolve, you are never locked in**. You can even mix these strategies, using a single version policy for most dependencies while allowing specific projects to maintain their own versions when necessary. Thanks to its smart dependency graph analysis, Nx can trace dependencies used by different projects and can therefore avoid unnecessary cache misses even when a root level lockfile changes, so that is not a concern.

Let's examine the trade-offs of each approach, using JavaScript/TypeScript as our primary example (though these principles apply to other languages as well):

## Independently Maintained Dependencies

Without tools like Nx, this is the only dependency maintenance strategy possible. Each project is responsible for defining which dependencies to use during development and which dependencies to bundle with the deployed artifact. For javascript, these dependencies are specified in a separate `package.json` file for each project. Each project's build command is responsible for bundling the packages in the `dependencies` section into the final build artifact. Typically packages are installed across the repo using yarn/npm workspaces.
In this model, each project maintains its own dependency definitions. For JavaScript/TypeScript projects, this means each project has its own `package.json` file specifying runtime dependencies, with development dependencies often still living at the root of the workspace (although they can also be specified at the project level). During builds, each project's bundler includes the necessary dependencies in its final artifact. Dependencies are typically managed using package manager workspaces (npm/yarn/pnpm/bun).

While this approach offers flexibility, it can introduce complexity when sharing code between projects. For example, if `project1` and `project2` use different versions of React, what happens when they try and share components? This can lead to runtime issues that are difficult to detect during development and challenging to debug in production.

A common pitfall occurs when developers have one version of a dependency in the root `node_modules` but a different version specified in their project's `package.json`. This can result in code that works locally but fails in production where the bundled version is used.

This strategy makes it very easy to have different versions of the same dependency used on different projects. This seems helpful because the code for each project exists in the same folder structure, but that code can't really be shared any more. If project1 and project2 use two different versions of React, what version of React will their shared code be written against? No matter how you answer, there will be bugs introduced in the system and often these bugs occur at runtime and are very difficult to diagnose.
**Pros:**

Another potential problem with this approach is that sometimes a developer may have one version of a dependency installed in the root `node_modules` and a different version specified in the project's `package.json`. This can lead to a scenario where the app works correctly on the developer's machine but fails in production with the bundled version of the dependency. This is once again a bug that is difficult to diagnose.
- Teams can independently choose and upgrade their dependencies
- More immediately clear what dependencies are intended for each project
- Easier transition for teams new to monorepos
- Modern tooling around e.g. module federation can help mitigate some of the challenges within applications

**Cons:**

- Complicates deployment when projects share runtime dependencies
- Makes code sharing between projects more challenging
- Can lead to hard-to-detect runtime conflicts
- Increases maintenance and strategy overhead with multiple versions to track

## Single Version Policy

With this strategy, developers define all dependencies in the root `package.json` file. This enforces a single version for all dependencies across the codebase, which avoids the problems listed above. Individual projects may still have `package.json` files, but they are used only for the metadata defined there, not as a way of defining the dependencies of that project.
This strategy centralizes all dependency definitions in the root `package.json` file, ensuring consistent versions across your codebase. While individual projects may still maintain their own `package.json` files for development purposes, the root configuration serves as the single source of truth for versions.

For building and deployment, you'll need to ensure each project only includes its relevant dependencies. Nx helps manage this through its workspace dependency graph and the `@nx/dependency-checks` ESLint rule, which can automatically detect and fix dependency mismatches between project and root configurations.

The main challenge with this approach is coordinating dependency updates across independent teams. When multiple teams work on different, or even the same, applications within the same repo, they need to align on dependency upgrades. While this requires more coordination, it often results in less total work - upgrading a dependency once across all projects is typically more efficient than managing multiple separate upgrades over time.

**Pros:**

- Ensures consistent dependency versions, preventing runtime conflicts
- Simplifies code sharing between projects
- Makes workspace-wide updates more manageable and easier to track

If there are React and Angular apps in the same repo, we don't want both frameworks bundled in the build artifacts of the individual apps. That's why the plugins Nx provides come with executors that use Nx's graph of dependencies to automatically populate the `dependencies` section of the individual `package.json` files in the build output and pre-populate a lock file for you. This enables your build artifacts to only contain the dependencies that are actually used by that app. As soon as a developer removes the last usage of a particular dependency, that dependency will be removed from the bundle.
**Cons:**

The primary concern people have with this approach is that of coordinating updates. If two different teams are working on React apps in the same repo, they'll need to agree about when to upgrade React across the repo. This is a valid concern and beyond the scope of Nx to solve. If the developers can't cooperate, they should probably work in separate repos. On the other hand, if the teams can agree, it becomes much less work to upgrade the whole repo at the same time rather than performing that same upgrade process multiple times spread out over months or years. Performing the same code manipulation 100 places all at once is much easier than remembering how to perform that code manipulation 100 different times spread out over a year.
- Requires coordination between teams for dependency updates
- May slow down teams that need to move at different velocities
- Needs stronger governance and communication processes

Consult the Nx executor's reference page to find options for populating dependencies and a lock file. If you would like to use Nx's graph to populate the dependencies of a project in your own scripts or custom executor, read about how to [prepare applications for deployment via CI](/ci/recipes/other/ci-deployment).
For details on using Nx's dependency graph in your deployment process, see our guide on [preparing applications for deployment via CI](/ci/recipes/other/ci-deployment).

0 comments on commit 2a82009

Please sign in to comment.