---
title: 'Dependency Trees and Conflicts'
source: 'https://academia.sh/en/courses/javascript-ecosystem/dependency-trees-and-conflicts'
course: 'Modules, Tooling and the Ecosystem'
language: en
updated: '2026-08-17T18:09:45+00:00'
license: 'CC BY-SA 4.0'
---

# Dependency Trees and Conflicts

How non-intersecting version requests are resolved by nested installation, the identity and state problems produced by duplicate copies, flattening the tree, and dependencies that require singularity.

The previous lesson examined a single range. A project holds dozens of packages, and
most of them carry their own dependencies. It is ordinary for the same package to be
requested from two different places with two different ranges.

If the requests intersect, there is no problem: a version is chosen from the
intersection and everyone uses it. What happens when they do not intersect? That is
this lesson's subject — the solution to that case, and its cost.

## Non-Intersecting Requests

The measurement project requests the second major version of a package named `shared`.
The `translator` package the project uses, however, depends on the first major version
of that same package. The intersection of the sets `^2.0.0` and `^1.0.0` is empty.

In system package managers, as described in the package manager lesson of the
Introduction to Linux course, this results in a conflict report: only one version of a
library can be installed system-wide. Module resolution, being location-dependent,
offers a different way out — two versions of the same package can sit at two separate
levels of the tree.

Two manifests show the conflicting requests in writing:

```json
{
  "name": "project", "version": "1.0.0", "type": "module",
  "dependencies": { "translator": "^3.1.0", "shared": "^2.0.0" }
}
```

```json
{
  "name": "translator", "version": "3.1.0", "type": "module", "main": "index.mjs",
  "dependencies": { "shared": "^1.0.0" }
}
```

The shape of the installed tree is this:

```
$ node tree.mjs
shared@2.0.0
translator@3.1.0
  shared@1.0.0
duplicate: shared -> 2.0.0, 1.0.0
```

The script that produces this output applies the search rule from the previous lesson
in reverse: it walks every `node_modules` directory and prints the manifests it finds.

```javascript
// file: tree.mjs
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';

async function walk(dir, depth = 0, findings = new Map()) {
  const moduleDir = join(dir, 'node_modules');
  let entries;
  try {
    entries = await readdir(moduleDir);
  } catch {
    return findings;
  }
  for (const name of entries.sort()) {
    const packageDir = join(moduleDir, name);
    const manifest = JSON.parse(await readFile(join(packageDir, 'package.json'), 'utf8'));
    console.log(`${'  '.repeat(depth)}${manifest.name}@${manifest.version}`);
    findings.set(manifest.name, (findings.get(manifest.name) ?? new Set()).add(manifest.version));
    await walk(packageDir, depth + 1, findings);
  }
  return findings;
}

const findings = await walk('.');
for (const [name, versions] of findings) {
  if (versions.size > 1) console.log(`duplicate: ${name} -> ${[...versions].join(', ')}`);
}
```

The placement in the tree is this: the second version of `shared` sits at the root
directory, the first version sits inside `translator`'s own `node_modules` directory.
By the search rule, an import made from inside `translator` looks in its own directory
first and finds the first version; an import made from the root finds the second
version. Both requests are satisfied.

## The Cost of Two Copies

The conflict is resolved, but the result is two separate package instances. The single
evaluation guarantee established in the module systems topic holds **per resolved file
path, not per package**; two different files mean two different modules.

```javascript
// file: node_modules/shared/index.mjs -- the second version
export const version = '2.0.0';
export class Record {
  constructor(name) { this.name = name; }
}
const records = [];
export function add(record) { records.push(record); return records.length; }
```

```javascript
// file: node_modules/translator/index.mjs -- uses the first version from its own directory
import { add, Record, version } from 'shared';

export function use(name) {
  const record = new Record(name);
  return { seenVersion: version, recordCount: add(record), record };
}
```

```javascript
// file: main.mjs -- uses the two packages above
import { add, Record, version } from 'shared';
import { use } from 'translator';

const ours = new Record('root');
console.log('version seen at root:', version, '| record count:', add(ours));

const theirs = use('inside-translator');
console.log('version seen by translator:', theirs.seenVersion, '| record count:', theirs.recordCount);
console.log('is the record translator produced an instance of our class:', theirs.record instanceof Record);
```

```
$ node main.mjs
version seen at root: 2.0.0 | record count: 1
version seen by translator: 1.0.0 | record count: 1
is the record translator produced an instance of our class: false
```

Three lines, three separate problems.

**Version visibility depends on who is asking.** Two different `shared` versions are
running in the same program. Which one runs depends on where the question is asked
from.

**State is not shared.** The two record lists are separate; both contain a single
element. Anything of the cache or counter kind that the package holds is split. If a
library offers something like "list all records," it only sees its own copy's records.

**Identity checks fail.** The object `translator` produces is not an instance of the
root's class. Even though the classes come from the same source, they are the product
of two separate evaluations; object identity does not know this. The same problem shows
up with symbols, error classes, and brand checks.

These three problems are a frequent cause of the "my program runs but behaves strangely
somewhere" kind of hard-to-diagnose failure. Looking for a duplicate version in the
dependency tree is the first step in diagnosing it.

## Flattening

Nested installation is not always necessary. If the requests intersect, a single copy
satisfies all of them, and that copy is placed at the root of the tree. This placement
is called **flattening**.

Flattening has two justifications. The first is space and time: a single copy is
installed instead of dozens of copies of the same package. The second is preventing the
three problems just listed.

A side effect of flattening is that it sets the stage for the sneaky situation touched
on in the previous lesson. A package hoisted to the root becomes visible even from
modules that do not write it in their own manifest. The code runs locally; when the
tree is built a different way — say another installation tool prefers nested placement
— the package disappears. Hence the explicit rule: every used package has to be
written in the manifest of the package using it.

## Dependencies Requiring Singularity

For some packages, two copies are not merely wasteful, they are an outright bug.
Libraries that keep a record, manage global state, or rely on object identity have to
be a single instance.

The mechanism that provides this is the **peer dependency** list introduced in the
first lesson. If a plugin writes the library it extends as a runtime dependency, it
installs its own copy into its own directory and duality is created. Written as a peer
dependency instead, it does not install the library; it only says "a version in this
range has to be present in the environment." Installing becomes the application's
responsibility, and a single copy remains in the tree.

The peer dependency range has to be chosen deliberately. Too narrow a range needlessly
shrinks the set of versions the plugin can be used with and produces non-intersecting
requests; too wide a range lets installation appear to succeed on versions where the
plugin does not actually work.

## When a Conflict Is Not Resolved

Nested installation does not resolve every conflict. In three situations a single copy
is mandatory and a decision has to be made:

**When peer dependencies do not intersect.** If two plugins want incompatible major
versions of the same library, the two cannot be used together. The fix is to upgrade
or replace one of the plugins.

**When values pass between packages.** If the object one package produces is given as
an argument to another package, and the receiving side does an identity check, two
copies do not work.

**When output size is decisive.** Carrying two versions of the same library in output
sent to the browser doubles the bytes downloaded.

In these situations, installation tools offer a manifest field that pins a transitive
dependency's version from outside. The field is useful but dangerous: forcing a
package to run on a version other than the one it expects deliberately violates the
version contract. When used it has to be justified with a comment and treated as
temporary; the lasting fix is upgrading the incompatible package.

## Summary

- Non-intersecting version requests are satisfied by installing two copies of the same
  package at different levels of the tree; the search rule is what makes each consumer
  find its own copy.
- The single evaluation guarantee holds per resolved file path, not per package; two
  copies keep separate state and fail object identity checks.
- Intersecting requests are satisfied with a single copy, hoisted to the root; this
  flattening also makes packages not written in the manifest become visible.
- Libraries requiring singularity are declared as peer dependencies; the responsibility
  of installing passes to the application and a single copy remains in the tree.
- Pinning a transitive dependency's version from outside violates the contract; it
  should be treated as temporary.

## Next Step

Up to here, every package came from outside. A team's own packages sit on the same
tree too, and the dependencies among them cannot wait for a publish on every change.
The next lesson covers the arrangement where multiple packages are kept in a single
repository, how that arrangement changes resolution, and how the build order across
packages is determined.
