Article

Modern Software World of Dependencies and Packages

A plain-language guide to software dependencies, npm vs pnpm vs Yarn, and how package managers work across languages.

10 min read
DependenciesNode.jsnpmDevOpsSecurity

When you build software today, you rarely write every line yourself. You install libraries that handle common jobs—logging in users, calling APIs, running tests, formatting dates—and plug them into your app. Each of those libraries is a dependency: outside code your project relies on to work.

That is normal and useful. It saves time and lets teams focus on what makes their product unique. The trade-off is that your app now depends on code maintained by other people, with their own update schedules, bugs, and security issues.

A dependency is not just one file you added—it is often a whole chain of packages, all the way down. Managing that chain well is what package managers are for.

What does a package manager do?

A package manager is the tool that finds, downloads, and tracks your dependencies. You tell it what you need (usually in a config file). It figures out compatible versions, installs them, and writes a lockfile so the same versions install on your laptop, your teammate’s machine, and in CI.

Without a lockfile, two developers can end up with different versions of the same library—and spend hours debugging “works on my machine” problems.

Direct vs. transitive dependencies

When you run npm install express, you add one direct dependency. Express itself depends on other packages. Those pull in more packages. That hidden chain is called transitive dependencies.

  • Direct — packages you explicitly list in your project file.
  • Transitive — packages your dependencies need, installed automatically.
  • Lockfile — a snapshot of the full resolved tree so installs stay reproducible.

A small app can still ship hundreds of transitive packages. That is why choosing a good package manager—and reviewing what you install—matters more than it might seem at first.


JavaScript and Node.js: npm, pnpm, and Yarn

In the JavaScript world, three tools cover most projects. They all read package.json, download from the npm registry, and produce a lockfile. Commands feel similar; the real differences are speed, disk usage, and how strictly they control what your code can import.

npm

npm ships with Node.js, so it is the default most tutorials and tools assume. It is the easiest place to start: install Node, run npm install, and you are ready.

Best for: new projects, small teams, and anything where “it just works out of the box” matters most.

Pros

  • Comes pre-installed with Node.js—no extra setup.
  • Largest ecosystem; almost every JS library supports it.
  • npx built in for running tools without global installs.
  • Steady improvements in speed and workspace support.

Cons

  • Uses more disk space than pnpm, especially across many projects.
  • Allows “ghost dependencies”—code can import packages you never declared.
  • Slower installs than pnpm on large projects.
  • package-lock.json merge conflicts are common in busy teams.

pnpm

pnpm keeps one shared copy of each package on your machine and links it into projects. That saves gigabytes if you work on multiple repos. It also uses a stricter folder layout, so you can only import packages you actually declared—a common source of hidden bugs with npm.

Best for: monorepos, teams with many projects, and anyone who wants fast installs plus a cleaner dependency tree.

Pros

  • Very disk-efficient—often saves GBs across a machine.
  • Strict tree prevents accidental imports of undeclared packages.
  • Among the fastest install times for large dependency trees.
  • Strong monorepo tools, built-in patching, and a readable YAML lockfile.

Cons

  • Not bundled with Node—you install it separately.
  • Some older tools expect a flat node_modules folder (less common today).
  • Monorepo hoisting behavior takes a little learning if you are new to it.

Yarn Classic (v1)

Yarn v1 was an early alternative to npm, popular for faster installs and stable lockfiles. Many older codebases still use it. Development has slowed; it is effectively in maintenance mode.

Best for: legacy projects already on Yarn v1—usually better to stay until you plan a migration.

Pros

  • Stable and well understood in long-running codebases.
  • Good offline caching compared to early npm versions.
  • Simple workspace commands for multi-package repos.

Cons

  • No active feature development—maintenance mode only.
  • No Plug’n’Play (PnP) support.
  • Less efficient than pnpm for disk and install speed.
  • Missing modern features newer tools offer.

Yarn Berry (v2+)

Yarn Berry is a full redesign. Its headline feature is Plug’n’Play (PnP): instead of a giant node_modules folder, Yarn maps imports directly to package locations. That enables zero-install workflows and very fast CI—but some tools still expect traditional node_modules.

Best for: large organizations with complex monorepos that can invest in migration and tooling setup.

Pros

  • PnP can eliminate install steps in CI entirely (zero-install).
  • Strict isolation between packages.
  • Workspaces with dependency constraints, offline mirrors, and plugins.
  • Advanced patching for fixing upstream packages.

Cons

  • PnP can break tools like React Native or certain ESLint setups.
  • Migration from Yarn v1 or npm takes deliberate effort.
  • Steeper learning curve than npm or pnpm.
  • Smaller community footprint compared to npm and pnpm today.

Which one should you pick?

There is no single “best” tool for every project. Match the choice to your situation:

  1. Just getting started? Use npm. It is already on your machine when you install Node.js.
  2. Working in a monorepo or want speed and disk savings? Use pnpm. It is the best balance for most teams today.
  3. Large enterprise monorepo with strict dependency rules? Consider Yarn Berry if your team can adopt PnP and handle migration.
  4. Already on Yarn v1? Stay on it until you have time to migrate to pnpm or Yarn Berry—do not switch mid-sprint without a plan.

You can use different tools on different projects. Switching is mostly about replacing the lockfile and updating a few config lines—not rewriting your application.


Package managers in other languages

JavaScript is not unique. Every major language has a standard way to manage third-party code. The file names change, but the idea is the same: declare what you need, lock versions, install reproducibly.

  • Composer (PHP) — manages PHP libraries via composer.json and composer.lock.
  • pip, Poetry, uv (Python) — pip comes with Python; Poetry and uv add lockfiles and faster, more predictable installs.
  • Cargo (Rust) — built into Rust; uses Cargo.toml and Cargo.lock. Often cited as one of the best-designed package managers.
  • NuGet (.NET) — packages for C# and the broader .NET ecosystem.
  • Maven and Gradle (Java, Kotlin) — resolve huge transitive trees for JVM projects.
  • Bundler (Ruby) — installs gems from a Gemfile, locked in Gemfile.lock.
  • Go modules — built into Go via go.mod and go.sum; no separate installer needed.

The habits transfer: commit your lockfile, scan for known vulnerabilities, and treat dependency updates as normal engineering work—not something to ignore until something breaks.


Keeping dependencies healthy

Whatever tool you use, a few practices keep dependency pain low:

  • Commit lockfiles — so every environment installs the same versions.
  • Update regularly — small, frequent patch updates are easier than rare giant upgrades.
  • Scan for vulnerabilities — run checks in CI; fix or document accepted risks.
  • Test before major upgrades — breaking changes often hide in transitive dependencies.
  • Question every new package — fewer dependencies means a smaller surface area to maintain and secure.
Dependencies are not the enemy—they are how modern software gets built. The goal is not to avoid them, but to choose them deliberately and keep them under control.