npm Builds node_modules, Maven Builds .m2: Position vs. Declaration in Module Resolution
The position-versus-name distinction drives storage duplication, reproducibility, and dependency-conflict costs. It's the reason JavaScript projects carry per-project node_modules, Java shares a global .m2, and pnpm's hardlink approach is becoming the default in Python tooling.
The divergence between npm and Maven comes down to whether module resolution uses implicit position or explicit declaration. Node's require() climbs from the current file looking for node_modules, never checking package.json dependencies, so the runtime only sees physically present files. That makes location the interface, forcing npm to copy dependencies into every project to satisfy the resolver. Maven instead reads groupId, artifactId, version from pom.xml, fetches from a repository, and places them in ~/.m2; the location is derived from coordinates, so it can be global and shared.
npm's history shows the consequences. v2 nested every dependency, causing duplication and path length issues. v3 flattened to reduce depth, but introduced phantom dependencies, unstable trees, and lockfiles. pnpm solves this without changing Node's rules: it uses a content-addressed store, hardlinks files into .pnpm, and symlinks declared packages, making location relocatable rather than unique. Java, Go, and Rust treat location as output, so global caches are free; Go and Rust even resolve at build time, producing static binaries. Python's venv and uv are moving toward the same pnpm pattern.
The underlying question is whether a module identifier is a name or a position. package.json exports, import maps, and the imports field are attempts to turn positions back into names. But node_modules will flatten, not disappear, unless resolution moves entirely to build time.
Calling module resolution 'position as a contract' turns historical quirks like phantom dependencies and lockfiles into a single design constraint, making pnpm's hardlink-and-symlink scheme an optimization rather than a semantic change.
The trade-off between runtime flexibility and build-time determinism is not zero-sum: Node's flexibility costs duplication, while Java's explicit classpath requires complex conflict resolution and reproducibility discipline.
The push toward build-time bundling in frontend tooling suggests node_modules may eventually become a build artifact rather than a runtime dependency, which would end the duplication problem without needing a new resolver.