Software dependency

On this page 7

A software dependency is an external component, such as a library, framework, runtime, tool or other package, that a piece of software requires in order to be built, installed or run. If the dependency is missing or present in an incompatible version, the program fails to compile, refuses to install or breaks at runtime.

In the area of installation & packages, dependencies are the reason package managers exist. A package does not ship everything it needs; it declares what it needs in a manifest, and the installer or package manager locates, downloads and installs those components. Managing dependencies well determines whether software is reproducible, secure and maintainable over its whole life.

How software dependencies are declared and resolved

Dependencies are declared in a machine-readable manifest that lists each required component and an acceptable version constraint. When software is installed, a package manager reads the manifest, performs dependency resolution to find a set of versions that satisfies every constraint, and installs them in the right order.

Resolution is harder than it looks because constraints interact. Choosing a newer version of one package may require a newer version of another, which in turn may conflict with a third. Finding a valid combination is, in general, a computationally hard problem, so modern package managers use backtracking or SAT-style solvers. Some ecosystems allow several versions of the same package to coexist in one installation, while others, including most Linux distributions and Python environments, permit only one version at a time, which makes conflicts more visible.

EcosystemManifestTypical lock file
JavaScript (npm)package.jsonpackage-lock.json
Pythonpyproject.toml or requirements.txtTool-specific lock files
Rust (Cargo)Cargo.tomlCargo.lock
Debian and Ubuntu packagescontrol file with Depends fieldNone; repository snapshot defines versions
RPM-based distributionsspec file with Requires fieldNone; repository defines versions
.NET (NuGet)Project file with PackageReferencepackages.lock.json
Where dependencies are declared

A lock file records the exact versions chosen during resolution, including indirect dependencies. Committing it ensures that every developer and every build server installs the same dependency tree, which is the basis of reproducible builds.

Types of dependencies

Not all dependencies play the same role. Classifying them correctly keeps installations lean and prevents development tools from reaching production systems.

TypeMeaningExample
Direct dependencyDeclared explicitly by the projectA web framework listed in the manifest
Transitive dependencyRequired by a dependency, not by the project itselfA parser used by that framework
Runtime dependencyNeeded when the program runsA database driver
Build-time dependencyNeeded only to compile or packageA compiler or code generator
Development dependencyNeeded only for testing or toolingA test runner or linter
Optional dependencyEnables extra features if presentAn image codec for an editor
System dependencyProvided by the operating systemA C runtime or a shared system library
Common kinds of software dependencies

Transitive dependencies usually outnumber direct ones by a wide margin. A project with a dozen declared libraries can easily pull in hundreds of packages, which is why dependency trees must be inspected with tools rather than by reading the manifest alone.

Version constraints and compatibility

Version constraints express which versions are expected to be compatible. Most ecosystems follow or approximate Semantic Versioning, in which a version MAJOR.MINOR.PATCH signals breaking changes in the major number, new compatible features in the minor number and fixes in the patch number.

ConstraintAcceptsTrade-off
Exact pin, =1.4.2Only that versionMaximum stability, no automatic fixes
Caret range, ^1.4.21.4.2 up to but excluding 2.0.0Receives features and fixes, small breakage risk
Tilde range, ~1.4.21.4.2 up to but excluding 1.5.0Receives patches only
Minimum, >=1.4Any later versionFlexible but exposed to breaking changes
WildcardAny version, written as an asteriskUnpredictable; avoid in production
Typical version constraint styles

Applications usually combine loose ranges in the manifest with a lock file, while libraries keep ranges broad so that they do not conflict with other libraries in the same project. When two components require incompatible versions of a shared dependency, the result is a diamond dependency conflict, one of the classic forms of dependency hell.

Ways of satisfying dependencies

A dependency can be shared through the system, bundled with the application or isolated in an environment. Each strategy trades disk space and update simplicity against reliability.

StrategyHow it worksMain advantageMain drawback
Shared system librariesOne copy used by many programsSingle place for security patchesVersion conflicts between programs
Static linkingLibrary code compiled into the binaryNo runtime dependencyEvery binary must be rebuilt for a fix
Bundling or vendoringCopies shipped inside the applicationPredictable behaviorLarger size, outdated copies
Isolated environmentsPer-project virtual environments or containersNo interference between projectsDuplication and image maintenance
Strategies for providing dependencies

On Windows, many applications depend on shared runtimes such as the Visual C++ Redistributable or the .NET runtime, which installers check and install first. Portable applications and container images instead carry their dependencies along, so they run consistently but must be updated as a whole.

Risks and good practice

Every dependency is code the project did not write but must trust. The most serious risks today concern the software supply chain rather than compatibility.

  • Supply chain attacks: compromised maintainer accounts, malicious updates and typosquatting packages with names close to popular ones.
  • Known vulnerabilities: an outdated transitive dependency can expose an application long after the direct dependency was reviewed.
  • Abandoned packages: unmaintained components stop receiving security fixes.
  • License obligations: copyleft licenses in the tree can impose conditions on distribution.
  • Good practice: commit lock files, scan dependencies automatically, keep a software bill of materials in SPDX or CycloneDX format and remove packages that are no longer used.

Stripped to the essentials

Most programs are not built entirely from scratch. They reuse ready-made parts written by others, such as code for reading images or connecting to the internet. Each of those parts is a dependency.

It is like a recipe that calls for store-bought puff pastry. You cannot bake the pie without it, and the wrong kind can spoil the result. The key takeaway is that software only works when the right parts in the right versions are present.

Where it is applied

A small team maintains an internal web application written in Python that declares 12 direct dependencies in pyproject.toml. A dependency scan shows that these pull in 87 packages in total, and one transitive library has a published security vulnerability.

The team updates the direct dependency that requires it, regenerates the lock file and runs the test suite, which reveals one breaking change in a date handling function. After a small code fix, they deploy the new version. They also add an automated weekly scan and a software bill of materials to their build pipeline, so the next vulnerable component is reported within days rather than discovered by accident months later.