skip to content

Dependency Resolution & Versioning

How a build or package manager takes your declared dependencies and computes one coherent set of versions, then installs it reproducibly. It is the source of a surprising share of real-world breakage, which is why interviewers ask about it.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

Imagine your application directly depends on library A and library B. Library A internally requires a shared library C at version 1.0, while library B internally requires that same library C at version 2.0. Only one version of C typically ends up available at runtime. What is this situation called, and why can't both versions simply be used side by side?

level: juniorimportance: must knowfreq 70%

answer

  1. diamond shape in the graph
  2. one classpath, one class name
  3. compiler checks its own version, runtime uses another
  4. Guava NoSuchMethodError story
  5. Jackson triplet must match

basics

~20 s

It's called a 'diamond dependency conflict.' Two things your app needs each secretly need a different version of the same third thing, but most systems can only load one version of a library at once, so something has to give.

solid answer

~50 s

This is the diamond dependency problem: the dependency graph forms a diamond shape — your app depends on A and B, both of which transitively depend on library C, but at different, potentially incompatible versions. It happens because most JVM/Node resolution models put dependencies on a single flat classpath or module namespace where only one version of a given library is normally loaded at a time — the JVM classloader keys classes by fully-qualified name, not name-plus-version, so two definitions of C's classes on the same classpath collide. Build tools like Maven and Gradle pick exactly one version of C for the whole build using a resolution strategy (e.g. 'nearest wins' or 'highest wins'). The chosen version might not be what A or B was actually built and tested against, so the build can succeed while runtime behavior silently changes, degrades, or breaks outright.

go deeper

for a junior

Should recognize the shape of the problem — two paths, one shared dependency, two versions — and know that only one version generally wins on a normal classpath.

for a middle

Should be able to explain mechanically why only one version 'wins' (classloader resolves by name, not name+version) and name at least one concrete runtime error symptom.

for a senior

Should discuss the trade-off between 'newest wins' vs 'oldest/pinned wins' defaults and connect the problem to semver compliance being a convention, not a guarantee.

for a principal

Should be able to generalize the failure mode across ecosystems (JVM classloading vs npm's nested resolution) and explain why deduplication for build hygiene is itself the root cause of the exposure.

## Where the name comes from A diamond conflict gets its name from the shape it draws in a dependency graph: - Your **application** sits at the top. - Branching down to two **direct dependencies** (`A` and `B`). - Which each branch further down to a shared **transitive dependency** (`C`) — draw that and you get a diamond. The problem is that A was built and tested against one version of C, B against another, and both versions claim the same identity (group + artifact name in Maven terms, or package name in npm/pip terms) but are different files with potentially different method signatures, class layouts, or behavior. Somewhere in the graph, a decision has to collapse those two versions into one. ## Why only one version can survive The reason this forces a hard choice, rather than just quietly working, comes down to how most runtime and build environments represent "a library." - On the JVM, a **classloader** resolves a class by its fully qualified name alone — `com.example.C` — with no version suffix. - If two JARs on the same classpath both define that class, the classloader typically loads whichever one it encounters first (often determined by classpath ordering) and silently ignores the other; every caller in the process, whether it came through A or B, gets that one definition. - Flat-resolution package managers behave similarly at the top level. This is fundamentally different from, say, two totally separate applications each with their own private copy of C — there, no conflict exists because there's no shared namespace. The diamond problem only arises because dependency management systems are explicitly trying to **deduplicate** and share library code across a project for the sake of build size, consistency, and (for the JVM) avoiding duplicate-class linkage errors. ## Why this is the norm and not an edge case Why does this situation exist at all, rather than being an edge case? Because dependency reuse is the whole point of a package ecosystem: - **Foundational libraries** (logging shims, collection utilities, JSON parsers, HTTP clients) sit at the bottom of thousands of other libraries' graphs. - Every one of those libraries pins or ranges its own **version expectation** independently, evolving on its own release cadence. - As a project's transitive dependency tree grows into the hundreds, the odds that two independent paths pulled the same shared leaf library at two different points in its release history approaches certainty on any project of real size. **Semantic versioning** is supposed to keep minor/patch bumps backward compatible, which reduces how often the collapsed version actually breaks something, but semver compliance is a convention enforced by discipline, not by tooling, so it is regularly violated in practice — a "minor" release removes a deprecated method a bit early, or a bug fix changes edge-case behavior another library silently depended on. ## The trade-off in resolving the diamond The trade-off in resolving the diamond is that whichever version the build tool ultimately selects, it is optimizing for a single global answer to a question that really has two different, locally correct answers (A wants 1.0, B wants 2.0). | Default | Why it appeals | What it costs | |---|---|---| | **Picking the newest version** | The most common default, because newer usually means more bug fixes and, if the library is careful about backward compatibility, is a superset of the old behavior | The cost is that "usually" is not "always," and A may have used a method the newer version actually removed | | **Picking the oldest version** | Protects the code that has been running the longest | May starve the other consumer of a fix it actually needs, and can leave the whole application on a known-vulnerable version of a security-sensitive library | Neither default is a real fix; both are just **tie-breaking heuristics** applied after the fact, with no guarantee of correctness for either original consumer. ## How it actually shows up in production In production, diamond conflicts show up as build-time surprises far less often than as runtime failures, because the compiler for each individual module only ever validated code against the version it was compiled against — not the version that actually ends up on the merged classpath. Typical symptoms include `NoSuchMethodError`, `NoSuchFieldError`, `AbstractMethodError`, or `ClassNotFoundException` thrown deep in a stack trace the first time a code path that touches the mismatched API executes. - Because that code path may not be exercised by every test suite, the break can lie **dormant** through code review and CI and only surface once a rarely used feature runs in production. - Worse, some mismatches don't throw at all — the method still exists but its behavior subtly changed, producing wrong output with no error signal whatsoever. ## Two well-known instances 1. **Google Guava.** A well-known real-world instance is the Google Guava library, which has a long history of removing or changing 'beta'/deprecated APIs across releases; applications that pull in two libraries each transitively depending on different Guava versions have repeatedly hit `NoSuchMethodError` in production once the build tool collapsed the graph to a single Guava jar that one of those libraries was never tested against. 2. **The Jackson JSON library family** (`jackson-core`, `jackson-databind`, `jackson-annotations`). Another common example, where the three artifacts must stay version-aligned; Jackson has since added an explicit runtime version check that throws a readable `Incompatible Jackson version` error rather than an opaque linkage failure, which is itself a deliberate mitigation for how often this class of problem otherwise fails silently or cryptically.

  • Why doesn't this happen the same way in ecosystems like npm, which nest node_modules and can install multiple versions of the same package?
    npm's nested node_modules layout lets two versions of the same package coexist as physically separate copies at different paths, so there's no single shared class-name slot the way the JVM has. The catch is that this only works cleanly for stateless, pure-code packages — if the package is meant to be a singleton (holds global state, or is checked with something like `instanceof`/identity comparisons across the app), having two separate copies breaks that assumption even though both technically 'installed' fine.
  • If the build succeeds with no errors or warnings, does that mean there's no diamond conflict risk in the project?
    No — a clean build only proves each individual module compiled against its own declared dependency version; it says nothing about whether the final merged classpath matches what each module actually assumed. Silent version collapsing is exactly why these failures show up at runtime instead of compile time, often only when a specific rarely-used code path executes.
  • Is 'highest version wins' always the safer default resolution strategy?
    It's the more common default and often the safer bet if the ecosystem takes semantic versioning seriously, since newer versions are more likely to be supersets of older behavior. It's not universally safe, though — a newer major version can remove or change APIs older consumers relied on, so 'highest wins' can just as easily break the older-pinned dependency instead of the newer one.

It's like two guests both bringing a dish that needs 'the oven' at a party — one wants it at 350°F for an hour, the other at 425°F for twenty minutes, but there's only one oven. Someone has to pick a setting, and whichever dish didn't get its setting comes out wrong even though nobody made a mistake preparing it.

saying these in an interview costs you the question

  • Claims the build tool can just 'use both versions' on the JVM without any classloader trick
  • Thinks a green build guarantees no version-skew risk at runtime
  • Confuses this with the transitive dependency graph structure itself rather than the version-collision aspect
  • Can't name a concrete runtime symptom (e.g. NoSuchMethodError)
  • Assumes semver bumps are always safe in practice

context

open as a page

In a Node.js project using npm, package.json lists dependency versions as semver ranges like ^18.2.0. What extra file does npm generate, and what specific problem does it solve that the ranges alone can't?

level: juniorimportance: must knowfreq 85%

basics

~20 s

A lockfile records the exact version of every package (including indirect ones) that got installed, so everyone who installs later gets the identical set of files instead of whatever the version ranges happen to resolve to that day.

open as a page

In Semantic Versioning (SemVer), a library bumps its published version from 2.3.5 to 2.4.0, and later from 2.4.0 to 3.0.0. What does each of those two jumps tell a consumer about what changed, and why does that difference matter when deciding whether to upgrade?

level: juniorimportance: must knowfreq 80%

basics

~10 s

SemVer is MAJOR.MINOR.PATCH. 2.3.5→2.4.0 (MINOR) means new features were added, safe to upgrade. 2.4.0→3.0.0 (MAJOR) means something incompatible changed - your code might break, so check the changelog first.

open as a page

A company publishes an internal package called `acme-auth-utils` on a private registry, but a developer's laptop still has the default public registry configured. One day a build silently pulls in a different `acme-auth-utils` package that was never written by anyone at the company. What attack is this, and how does the resolution logic actually let it happen?

level: juniorimportance: must knowfreq 60%

basics

~20 s

An attacker publishes a public package with the exact same name as a company's private internal package. If a build tool checks the public registry and grabs whichever version looks newest/highest, it installs the attacker's fake package instead of the real internal one — no phishing or malware download needed, just a naming collision.

open as a page

In a build tool's dependency manifest (like package.json, pom.xml, or build.gradle), what is the difference between a 'direct' dependency and a 'transitive' dependency, and where do transitive dependencies come from?

level: juniorimportance: must knowfreq 75%

basics

~10 s

A direct dependency is a library you add yourself. A transitive dependency is a library that gets pulled in automatically because one of your direct dependencies needs it to work.

open as a page

What does it mean to 'vendor' a dependency into a repository, and why might a team choose to do that instead of pulling it from a package registry at build time?

level: juniorimportance: must knowfreq 55%

basics

~10 s

Vendoring means copying a library's source or compiled code directly into your own project's repo instead of fetching it from the internet each build, so the build always has exactly that copy available.

open as a page

In an npm package.json, what's the difference between a caret range like ^2.3.1 and a tilde range like ~2.3.1, and how does that change for a 0.x.y version like ^0.2.3?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Caret (^) lets a dependency update to any newer version that keeps the first non-zero number the same — bigger updates allowed. Tilde (~) only lets the last number change — tiny patch updates only. For versions starting with 0, caret becomes as strict as tilde.

open as a page

In a build tool's transitive dependency graph, what does the 'nearest wins' rule do when two paths pull in different versions of the same library?

level: juniorimportance: must knowfreq 70%

basics

~10 s

When the same library is needed in different versions, the build tool picks whichever version is declared closest to your own project (fewest hops away), not the newest one.

open as a page

A project compiles cleanly against library X version 1.5, but after the build tool resolves a transitive version conflict, X version 2.0 ends up on the runtime classpath instead. At runtime, the code throws a NoSuchMethodError. Walk through why this happens even though nothing failed during compilation.

level: middleimportance: must knowfreq 75%

basics

~20 s

The code was checked at build time against one version of the library, but a different version actually runs. If that version removed or changed something the code relies on, the program crashes trying to use it — the compiler never saw the version that actually ships.

open as a page

A lockfile entry for a dependency includes a field like integrity: sha512-abc123.... What is this value used for during npm install, and what specific class of attack or failure does it guard against?

level: middleimportance: must knowfreq 65%

basics

~20 s

It's a cryptographic fingerprint of the exact package contents; before installing, the tool re-hashes what it downloaded and compares it to this stored value, refusing to install if they don't match -- catching tampered, corrupted, or substituted packages.

open as a page

A team is deciding whether to write exact pinned versions in their dependency manifest (no ranges at all) or keep caret/tilde ranges in the manifest while relying on a committed lockfile for reproducibility. What do they actually gain or lose with each approach?

level: middleimportance: must knowfreq 80%

basics

~20 s

Exact pins in the manifest and ranges-plus-lockfile both give you the same reproducible install day to day; the real difference shows up when you deliberately want to bump -- pins force you to touch every version number by hand, while ranges let a lockfile update or an automated bot pull in allowed upgrades with one command.

open as a page

A published library's public API compatibility promise under Semantic Versioning covers more than just function signatures. Besides removing or renaming an exported function, name two other kinds of changes that count as 'breaking' under SemVer and explain why each breaks the promise even though no code was deleted.

level: middleimportance: must knowfreq 85%

basics

~20 s

Breaking changes aren't just deleted functions. Changing what a function returns, tightening validation so old inputs now error, or changing a default behavior can all break callers even though the function still exists - because existing code that worked now behaves differently or fails.

open as a page

Your application's package.json lists 40 direct dependencies, all from well-known, actively maintained projects. But `npm ls --all` shows the dependency tree resolves to over 1,200 packages once transitive dependencies are counted. Why does that transitive depth matter for supply-chain risk, even though every direct dependency looks trustworthy?

level: middleimportance: must knowfreq 60%

basics

~20 s

Each transitive dependency is code your app runs, chosen and updated by people you've never vetted, several layers removed from anything you reviewed. A well-run 40-dependency project can still pull in a single compromised package buried five layers deep, and it runs with the same privileges as everything else.

open as a page

A developer runs `pip install reqeusts` (missing a letter) instead of `pip install requests`, and the install succeeds without any warning. What class of supply-chain attack does this expose, and what actually stops it from working in practice?

level: middleimportance: must knowfreq 55%

basics

~20 s

Typosquatting: an attacker registers a package name that's a common misspelling or near-miss of a popular one (like reqeusts for requests). Anyone who mistypes the real name installs the attacker's package instead, which can run malicious code the moment it's installed.

open as a page

Walk through, step by step, how a package manager builds the full dependency graph for a project once it starts from the top-level manifest - what does it actually do at each step?

level: middleimportance: must knowfreq 70%

basics

~10 s

It reads your manifest, fetches each listed package, reads that package's own manifest, fetches its dependencies too, and keeps repeating that until there's nothing new left to fetch.

open as a page

When you build a 'fat jar' (also called an uber jar) for a Java application, what actually goes into that archive, and why is it needed compared to a normal 'thin' application jar?

level: middleimportance: must knowfreq 65%

basics

~20 s

A fat jar packs your compiled code together with the compiled code of every library it depends on into one single file, so you can run it anywhere with just java -jar, no separate downloads needed.

open as a page

What's the practical trade-off between pinning a dependency to an exact version versus letting it float within a range, and how do teams get the benefits of both?

level: middleimportance: must knowfreq 75%

basics

~20 s

Pinning gives you a build that's exactly reproducible every time, but you miss bug fixes and security patches until someone manually bumps it. Floating gets those fixes automatically, but the exact code you're running can silently change. Most teams use a lockfile to get both: loose ranges for humans, exact pins for machines.

open as a page

When a build's automatic version mediation (nearest-wins or highest-wins) picks the wrong version, what's the practical difference between excluding a transitive dependency, forcing/pinning a specific version, and declaring a centralized version-management constraint - and when would you reach for each?

level: middleimportance: must knowfreq 60%

basics

~20 s

Exclusion removes a library from the graph entirely; forcing/pinning locks one exact version everywhere it appears; a centralized override sets a preferred version without removing anything. Use exclusion to cut something out, pinning to lock a known-good version, and centralized overrides to align a whole codebase.

open as a page

How do 'nearest wins' and 'highest version wins' differ as strategies for mediating conflicting transitive dependency versions, and what's a concrete trade-off between them?

level: middleimportance: must knowfreq 65%

basics

~10 s

Nearest-wins picks whichever version is declared closest to your project; highest-version-wins picks whichever version number is biggest, no matter how deep it's buried. One favors what you explicitly asked for, the other favors freshness.

open as a page

A multi-module Maven or Gradle build has dozens of modules that each independently declare their own version of shared libraries like Jackson and Guava, causing frequent diamond conflicts across the project. How does adopting a Bill of Materials (BOM) or platform mechanism help with this, and what problem does it NOT solve on its own?

level: seniorimportance: must knowfreq 65%

basics

~20 s

A BOM is a shared list that says 'when any module in this project uses library X, use exactly this version.' It stops different modules from picking different versions in the first place. It doesn't fix cases where the pinned version itself turns out to be broken or incompatible with something.

open as a page

A published npm package (a library consumed by other projects) does not commit a lockfile, while the internal web application that depends on it does commit one. Why does this convention differ between libraries and applications?

level: seniorimportance: must knowfreq 75%

basics

~20 s

An app is the final thing that actually runs, so it needs one fixed, tested set of dependency versions locked down. A library gets installed inside many different apps' own dependency trees, so locking its own versions would fight with -- and often duplicate -- whatever versions the consuming app already resolved.

open as a page

A team runs Dependabot/Renovate configured to auto-merge any dependency upgrade that stays within its existing SemVer range (i.e., MINOR/PATCH bumps only) once CI passes. Six months in, a MINOR bump auto-merges and a production incident follows. Walk through why SemVer-based auto-merge can still produce this outcome, and what would reduce the risk without abandoning automation entirely.

level: seniorimportance: must knowfreq 70%

basics

~20 s

SemVer numbers are a promise, not a guarantee - a maintainer can mislabel a change, or your tests might not cover the exact behavior that changed. Auto-merging on version number alone can still let a real break through if your CI doesn't actually exercise that code path.

open as a page

A customer's procurement team tells your team they won't approve a new deployment until you provide an SBOM (Software Bill of Materials) for the service. What is an SBOM, and what specific supply-chain scenario does having one actually let your team respond to faster?

level: seniorimportance: must knowfreq 55%

basics

~20 s

An SBOM is a complete, machine-readable inventory of every software component — direct and transitive — that makes up an application, including exact versions and where they came from. It matters because when a component turns out to be compromised or vulnerable, you can instantly check 'do we use it, and where' instead of manually digging through every service.

open as a page

A security team wants to know, for every service in a company's portfolio, whether any dependency anywhere in the resolved graph carries a copyleft license (like GPL) or a known CVE. Why is this materially harder than just checking each service's manifest file, and what has to be built to answer it reliably?

level: seniorimportance: must knowfreq 65%

basics

~20 s

The manifest only lists the top-level libraries a team chose - it says nothing about the hundreds of other libraries those libraries secretly depend on. To really check licenses or vulnerabilities you have to look at the entire expanded graph, not just the short list a human wrote.

open as a page

What does 'shading' (class relocation) do when packaging a JVM application, and what specific problem does it solve that simply choosing one dependency version cannot?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Shading renames a library's internal package names inside your packaged app (like turning com.google.guava into myapp.shaded.guava) so two different copies of that library, needed by different parts of your app, can both exist at once without clashing.

open as a page

Semver's compatibility promise says a minor or patch bump should never break a consumer. In practice, why can upgrading within a caret or tilde range still break a build, and what upgrade-risk mitigations do teams actually rely on?

level: seniorimportance: must knowfreq 60%

basics

~20 s

Semver is a promise maintainers make, not something enforced by any tool, so a 'safe' minor or patch release can still accidentally change behavior. Teams protect themselves with automated tests, staged rollouts of upgrades, and reviewing changelogs before merging a version bump, rather than trusting the range alone.

open as a page

As a tech lead deciding your org's default policy on vendoring dependencies versus resolving them dynamically from a registry, what factors should actually drive that decision, and what does each choice cost you operationally?

level: principalimportance: must knowfreq 45%

basics

~20 s

Copying dependency code into your repo makes builds more reliable and reproducible but bloats the repo and makes it your job to notice security fixes; pulling from a registry each time stays lean and easy to patch but depends on that registry always being available and unchanged.

open as a page

Given the two version strings `1.5.0-beta.2` and `1.5.0+build.20260104`, what does the `-beta.2` suffix versus the `+build.20260104` suffix each mean under the SemVer spec, and which one (if either) affects how two versions compare for precedence?

level: middleimportance: should knowfreq 55%

basics

~20 s

The dash part (-beta.2) means it's a pre-release, not yet the real 1.5.0, and it does affect ordering - it sorts before 1.5.0. The plus part (+build...) is just extra build info like a commit hash, ignored when comparing versions.

open as a page

In a resolved dependency graph, what do 'fan-out' and 'depth' mean, and why does a project with the same total dependency count but a different fan-out/depth shape behave differently for someone trying to understand it?

level: middleimportance: should knowfreq 55%

basics

~20 s

Fan-out is how many other libraries a single package pulls in directly. Depth is how many layers of 'depends on a depends on b depends on c' you have to walk before you stop finding new packages. A graph can be shallow-but-wide or narrow-but-deep, and each is confusing in a different way.

open as a page

In Maven/Ivy-style dependency version ranges written like [1.0,2.0) or (,1.5], what do the square brackets versus parentheses mean, and how does this differ from npm's caret/tilde model?

level: middleimportance: should knowfreq 40%

basics

~10 s

Square brackets mean 'include this exact boundary,' parentheses mean 'up to but not including it.' So [1.0,2.0) means from 1.0 up to (not including) 2.0. It's a math-style interval instead of npm's shorthand symbols.

open as a page

showing 1–30 of 44