Maven — Guava 32 Transitive Conflict
NoSuchMethodError on Guava 32.0 caused by Maven's nearest-wins selecting Guava 31.0 from transitive dep.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- Maven uses GAV coordinates (groupId:artifactId:version) to identify every dependency
- Transitive dependencies are automatically resolved — nearest-wins determines the version when conflicts exist
- DependencyManagement sets default versions for child modules without adding dependencies (BOMs follow the same pattern)
- Scopes (compile, test, provided, runtime) control classpath inclusion and packaging
- Production failure: a transitive dependency with a different version causes NoSuchMethodError at runtime, often only in prod due to differing classpaths
- Biggest mistake: assuming nearest-wins picks the 'best' version — it picks the shortest path, which can be older and incompatible
Maven is a build tool that manages your Java project's dependencies — the external libraries your code needs. Instead of manually downloading JARs and putting them in a lib folder, you declare what you need in a pom.xml file and Maven downloads and manages them for you. It's like ordering ingredients for a recipe: you write the shopping list, Maven goes to the store, checks for compatible versions, handles the sub-ingredients (transitive dependencies), and puts everything in your kitchen (local repository). The shopping list is your pom.xml, the store is Maven Central, and your kitchen is ~/.m2/repository.
Every non-trivial Java project depends on external libraries — Spring, Jackson, JUnit, Lombok, and dozens more. Before Maven, developers manually downloaded JARs, dropped them into a lib folder, and manually resolved version conflicts. Maven (and Gradle after it) replaced that chaos with a declarative, reproducible build system. This guide explains how Maven dependency management actually works — scopes, transitive dependencies, conflict resolution, BOMs, multi-module projects, Maven Wrapper, security scanning, and the Maven lifecycle — so you can debug build problems and structure projects correctly.
I've seen teams lose days to dependency hell: a transitive dependency pulled in an incompatible version of Guava, which caused a NoSuchMethodError at runtime that only manifested in production because the test environment had a different classpath. Understanding Maven's dependency resolution isn't academic — it's the difference between a clean build and a 3 AM debugging session.
Why Maven Dependency Management Is Not Optional
Maven dependency management is a centralized mechanism to control the versions of transitive dependencies across a multi-module project. Instead of each module declaring its own version for a library like Guava, you define the version once in a <dependencyManagement> section in the parent POM. All child modules then inherit that version automatically, overriding any transitive version that would otherwise be pulled in.
This works because Maven uses a nearest-wins strategy: the version declared closest to the project (in the dependency tree) takes precedence. By placing a version in <dependencyManagement>, you effectively pin that version for every module, regardless of what transitive dependencies bring in. This eliminates version conflicts and ensures a single, consistent classpath.
Use dependency management when your project has multiple modules or depends on libraries that share common transitive dependencies. Without it, you risk classpath hell — different modules loading different versions of the same class, leading to NoSuchMethodError at runtime. In production, this is the difference between a clean deploy and a midnight pager.
Hashing.sha256() during a critical batch job.Hashing.sha256()Lcom/google/common/hash/HashFunction; — because Guava 27's Hashing class lacked the sha256() method added in Guava 28.SNAPSHOT vs Release Versions — The Difference That Breaks Reproducibility
You'll notice the version in the pom.xml above is 1.0.0-SNAPSHOT. That -SNAPSHOT suffix is not a placeholder or a comment — it has specific, important behavior that every Maven developer must understand.
A SNAPSHOT version is a version under active development. When Maven encounters a SNAPSHOT dependency, it checks the remote repository for a newer version on every build (by default, once per day). This means the artifact can change between builds. A release version (no -SNAPSHOT suffix) is immutable — once published to a repository, it never changes. Maven caches it locally and never checks for updates.
The danger of SNAPSHOT dependencies: If your production build depends on a SNAPSHOT version of another team's library, your build is non-reproducible. Monday's build might pull version 2.3.1-SNAPSHOT from Tuesday at 10 AM, while Wednesday's build pulls 2.3.1-SNAPSHOT from Wednesday at 3 PM — same version string, different contents. This is how subtle, intermittent bugs creep in. I once debugged a flaky integration test for two days before discovering that a SNAPSHOT dependency had been updated between CI runs, changing the serialization format of a DTO.
When to use SNAPSHOT: During active development of your own modules. When your team is building module A and module B together, and module B depends on module A, both use SNAPSHOT during development. When you're ready to release, you strip the -SNAPSHOT, tag in Git, deploy to the release repository, and bump to the next SNAPSHOT.
When to NEVER use SNAPSHOT: In production deployments. In any released artifact. In any dependency declared in a library that others consume. If you publish a library to Maven Central with a SNAPSHOT dependency, consumers will get unpredictable builds.
The release process: Maven has a release plugin that automates this. mvn release:prepare strips the -SNAPSHOT, commits, tags in Git, bumps to the next version, and adds -SNAPSHOT to the new version. mvn release:perform builds and deploys the tagged version. This is the standard workflow for releasing Maven artifacts.
Update policies: By default, Maven checks for SNAPSHOT updates once per day. You can change this in settings.xml with <updatePolicy>always</updatePolicy> (check every build), <updatePolicy>daily</updatePolicy> (default), <updatePolicy>interval:X</updatePolicy> (every X minutes), or <updatePolicy>never</updatePolicy> (only use cached). Use -U on the command line to force an update check for one build. Use -o (offline) to skip all remote checks.
The Maven Wrapper — Guaranteeing Consistent Builds
Every developer on a team might have a different version of Maven installed. Compiling with Maven 3.8.6 might work fine, but running with Maven 3.9.6 on CI could break your build due to API changes or plugin incompatibilities. This is a classic 'works on my machine' problem.
The Maven Wrapper solves this. It ships your project with a script (mvnw for Unix-like, mvnw.cmd for Windows) that downloads and runs a specific Maven version declared in .mvn/wrapper/maven-wrapper.properties. This ensures every developer, and every CI server, uses the exact same Maven version for your project, guaranteeing consistent builds.
How to add it: Run mvn wrapper:wrapper in your project root. This generates the scripts and the wrapper JAR. You must commit the scripts and the .mvn directory to Git.
Using it: From then on, always use ./mvnw (or mvnw.cmd on Windows) instead of just mvn. This ensures you're always using the wrapper's pinned Maven version.
Configuration: The .mvn/maven.config file lets you pass arguments to every Maven invocation (e.g., -T 1C for parallel builds, -Dstyle.color=always to force colors in CI). The .mvn/jvm.config file lets you pass JVM arguments to the Maven process itself (e.g., -Xmx4g for increased heap size).
The .mvn directory: The .mvn/ directory is also where you put Maven configuration files like .mvn/maven.config (command-line arguments applied to every build, e.g., -T 1C for parallel builds) and .mvn/jvm.config (JVM arguments for the Maven process itself, e.g., -Xmx2g). These are also committed to Git, ensuring consistent build behavior across all environments.
Transitive Dependencies — What Maven Resolves For You
When you add spring-boot-starter-web to your pom.xml, you are adding one entry. But spring-boot-starter-web itself depends on spring-webmvc, which depends on spring-context, which depends on spring-core, which depends on spring-jcl. Maven resolves this entire tree automatically.
These are transitive dependencies — libraries your dependency depends on. Maven downloads the full closure of dependencies needed at runtime. For a typical Spring Boot application, that means 50-100+ JARs managed automatically.
The risk: transitive dependencies can include versions that conflict with each other or with your own declarations. Maven uses nearest-wins to resolve conflicts: the dependency closest to the root of the tree (lowest depth) wins. If you declare a version explicitly in your own pom.xml, your version wins over any transitive version.
The real-world gotcha: nearest-wins doesn't mean 'best version wins.' It means 'shortest path wins.' If library A pulls in Guava 31.0 at depth 2, and library B pulls in Guava 32.0 at depth 3, you get Guava 31.0 — the older version. If Guava 32.0 has a method that library B calls, you get a NoSuchMethodError at runtime. This is the single most common source of cryptic runtime errors in Maven projects. The fix is to declare the version explicitly in your own pom.xml or in dependencyManagement, which always wins (depth 1).
Exclusions: Use <exclusions> to drop a transitive dependency you don't need. Common scenarios: switching from Tomcat to Jetty (exclude spring-boot-starter-tomcat, add spring-boot-starter-jetty), removing a logging bridge (exclude commons-logging when using SLF4J), or dropping a vulnerable transitive library.
mvn dependency:tree and check what it brings in. I've seen a single utility library pull in 40 transitive dependencies including an older version of Spring, a conflicting logging framework, and an XML parser with a known CVE. Know what you're importing before it's in your classpath.Dependency Scopes — compile, test, provided, runtime, system
Every dependency has a scope that controls when it is on the classpath and whether it is included in the final packaged artifact.
compile(default): The dependency is on the classpath at compile time, test time, and runtime. It is packaged in your JAR/WAR. This is the right scope forspring-boot-starter-web,jackson-databind, and most application libraries.test: Only on the classpath during test compilation and execution. Not packaged. Use for JUnit, Mockito, AssertJ, H2 (in-memory test database). If a test-scoped dependency ends up in your production bundle, something is wrong — it bloats the artifact and may introduce classpath conflicts.provided: On the classpath at compile time but not packaged — the runtime container provides it. Use forservlet-apiin WAR deployments where Tomcat provides the servlet API. In modern Spring Boot fat-JAR deployments, you rarely need this because Spring Boot embeds the container.runtime: Not needed at compile time but needed at execution. The classic example is a JDBC driver — your code compiles againstjava.sql.Driver(the interface), but the MySQL driver JAR is only needed at runtime. Another example: Jackson annotations arecompile-scoped, but the Jackson databind runtime isruntime-scoped.system: Similar toprovidedbut points to a JAR on the local filesystem via<systemPath>. This is deprecated and should never be used in new code. It's non-portable — the path is machine-specific. If you encounter it in legacy code, migrate by runningmvn install:install-fileto install the JAR into your local repository, then use a normalcompilescope. Better yet, deploy it to your company's Nexus/Artifactory.
Scope inheritance rules: A compile-scoped dependency propagates transitively as compile. A test-scoped dependency never propagates transitively. A provided-scoped dependency propagates as provided. A runtime-scoped dependency propagates as runtime.
Optional Dependencies — The Table Row You Never Explained
Your comparison table includes 'optional' but the article never explains it. Let's fix that.
An optional dependency is a dependency that your library can use but doesn't require. When you mark a dependency as <optional>true</optional>, it is NOT propagated to consumers. Projects that depend on your library won't automatically get that dependency — they have to declare it explicitly if they need it.
The use case: Your library supports multiple database drivers (PostgreSQL, MySQL, H2) but doesn't require any specific one. You mark each driver as optional. Consumers add the one they need. Without optional, a consumer who adds your library would get ALL database drivers pulled in transitively, even if they only use PostgreSQL.
The gotcha: Optional is NOT a scope. It's a flag that can be applied to any dependency regardless of scope. A compile-scoped dependency marked optional is still on your classpath during compilation — it just doesn't propagate to consumers. A common mistake is thinking optional means 'not needed at runtime' — it doesn't. It means 'not forced on consumers.'
When to use it: Library authors who support multiple backends. Framework authors who provide extensions. Any situation where a dependency is one of several possible alternatives rather than a requirement.
The Maven Build Lifecycle — What Happens When You Run mvn install
Maven defines a standard build lifecycle: a sequence of phases executed in order. The most important lifecycle is default (build and package your project). Running any phase triggers all preceding phases automatically.
Key phases in the default lifecycle (in order): validate (check project is correct), compile (compile source code into target/classes), test-compile (compile test source), test (run unit tests; build fails if tests fail), package (create the distributable artifact, e.g., JAR/WAR in target/), verify (run integration tests), install (copy artifact to local ~/.m2/repository), deploy (copy artifact to remote repository for sharing).
mvn clean removes the target/ directory. mvn clean install is the most common command: it cleans stale files, compiles, tests, packages, and installs to local repo. Use -DskipTests or -Dmaven.test.skip=true to skip tests during packaging (but only when necessary — do not make this a habit).
The build directory structure: After mvn package, your target/ directory contains: target/classes (compiled main source), target/test-classes (compiled test source), target/surefire-reports (test results), target/my-app-1.0.0.jar (the packaged artifact). The JAR contains only your compiled classes — dependencies are NOT included unless you use the shade or assembly plugin (or Spring Boot's repackage goal for fat JARs).
Properties and Version Management — Centralising Version Numbers
Hard-coding version numbers across a pom.xml makes upgrades painful. Use Maven properties to centralise versions in one place. Define a property in <properties>, reference it with ${property.name}.
Spring Boot's parent POM defines hundreds of version properties (jackson.version, logback.version, slf4j.version, etc.) that you can override simply by redefining the property. This is how you upgrade a transitive library version without an exclusion.
For multi-module projects, define all version properties in the root parent POM. Child modules never hardcode versions — they use properties inherited from the parent or versions managed in the parent's dependencyManagement section.
The override trick: If Spring Boot manages Jackson at version 2.15.3 but you need 2.16.0, you don't need an exclusion or a direct version declaration. Just add <jackson.version>2.16.0</jackson.version> to your <properties>. Spring Boot's dependencyManagement references the ${jackson.version} property, so your override propagates to all Jackson artifacts automatically.
Custom properties for plugins: Properties aren't just for dependency versions. Use them for plugin configuration too — Java version, encoding, skip flags, etc.
Maven Plugins — The Build Engine You Configure But Rarely See
Maven's behavior is defined by plugins. Every lifecycle phase is bound to a plugin goal. You don't need to configure most of them — Maven's default bindings handle compilation, testing, and packaging. But understanding the key plugins helps you debug build problems and customise behavior.
maven-compiler-plugin: Compiles Java source code. Configures the Java version (source and target), annotation processor paths (for Lombok, MapStruct), and compiler arguments. In Java 21+ projects, you'll configure this to set the release flag instead of source/target.
maven-surefire-plugin: Runs unit tests during the test phase. Configures test inclusion/exclusion patterns, parallel execution, JVM arguments for tests, and test failure behavior. If your tests aren't running, check the surefire configuration.
maven-failsafe-plugin: Runs integration tests during the verify phase. Separate from surefire — integration tests are typically named *IT.java and run after packaging. This separation lets you run unit tests quickly during development (mvn test) and integration tests only in CI (mvn verify).
maven-jar-plugin: Creates the JAR file. Configures the MANIFEST.MF — Main-Class for executable JARs, Class-Path for dependency references. In Spring Boot projects, the spring-boot-maven-plugin overrides this to create fat JARs.
spring-boot-maven-plugin: Creates executable fat JARs that embed Tomcat/Jetty and all dependencies. The repackage goal takes the standard JAR and repackages it with a custom classloader structure.
maven-enforcer-plugin: Enforces rules on your build. Common rules: requireMinimumMavenVersion, banDuplicateClasses, requireJavaVersion, bannedDependencies. I add this to every project — it catches problems before they reach production.
maven-versions-plugin: Manages dependency versions. display-dependency-updates checks for newer versions. use-latest-releases auto-updates (use with caution).
maven-enforcer-plugin catches problems that otherwise surface as cryptic runtime errors. banDuplicateClasses finds JARs that contain the same class (a common source of NoSuchMethodError). requireMavenVersion ensures all developers and CI use compatible Maven versions. Add it to your parent POM so all child modules inherit it.Maven Profiles — Conditional Configuration for Different Environments
Profiles let you define conditional configuration that activates based on environment, JDK version, OS, or explicit command-line selection. They're Maven's answer to 'I need different settings for dev vs CI vs production.'
How profiles work: A profile is defined in <profiles> and contains any subset of the POM — dependencies, plugins, properties, repositories. When the profile is active, its contents are merged into the main POM. When inactive, they're ignored.
Activation methods: Command-line: mvn install -Pdev activates the 'dev' profile. JDK version: <activation><jdk>[21,)</jdk></activation> activates on JDK 21+. OS: <activation><os><family>unix</family></os></activation> activates on Linux/macOS. Property: <activation><property><name>env</name><value>ci</value></property></activation> when -Denv=ci is passed. * File exists: <activation><file><exists>docker-compose.yml</exists></file></activation>.
Common use cases: Different database drivers for dev (H2) vs production (PostgreSQL). Enabling/disabling features. Different repository configurations for different networks.
The gotcha: Profiles defined in your pom.xml are not inherited by child modules unless they're in the parent POM's <profiles> section. Activation is per-module — activating a profile on the parent doesn't automatically activate it on children unless you use -P on the command line, which activates it project-wide.
Maven BOM (Bill of Materials) — Centralized Dependency Version Management
When you have multiple modules that share common dependencies, version drift becomes a real pain. A BOM (Bill of Materials) is a special POM that centralizes dependency version declarations without adding any dependencies to the classpath. It's a catalog of versions that child modules reference using <scope>import</scope> in <dependencyManagement>.
How it works: A BOM POM has a <dependencyManagement> section with <dependencies> and their versions. Other projects import this BOM into their own <dependencyManagement> using <type>pom</type><scope>import</scope>. Once imported, they can declare dependencies from the BOM without specifying versions — the version comes from the BOM.
Spring Boot's BOM: When you use spring-boot-starter-parent, it already imports spring-boot-dependencies BOM. That BOM manages over 200 dependency versions (Spring Framework, Jackson, Hibernate, etc.). Override any version via a property.
Creating your own BOM: For a company with microservices, a BOM in a shared repository ensures every service uses the same version of Guava, Jackson, Logback, etc. Just release a BOM POM and have all projects import it.
Gotcha: A BOM's <dependencyManagement> is not inherited transitively. If BOM A imports BOM B, projects that import only BOM A do NOT automatically get BOM B's managed versions. You must explicitly import both BOMs if you need both sets of versions.
Maven Settings.xml — Private Repositories, Mirrors, and Authentication
Maven's settings.xml (located in ~/.m2/settings.xml or in the project's .mvn/ directory) configures global build behavior. The two most common uses are repository authentication and mirrors.
Private repositories: Most companies run an internal Maven repository (Nexus, Artifactory, or AWS CodeArtifact) to host internal libraries and proxy external repositories. You configure these in settings.xml or in the POM's <repositories> section. For authentication, use settings.xml with a <server> entry that references the repository ID.
Mirrors: A mirror intercepts all requests for a repository and redirects them to another URL. Common setup: mirror Maven Central to your internal proxy to speed up builds and control external access. Mirror configuration is in settings.xml under <mirrors>.
Never hardcode credentials in pom.xml. Always use settings.xml or CI environment variables with -D parameters. Committing credentials to Git is a security incident waiting to happen.
Profiles in settings.xml: You can define profiles in settings.xml that are active for all projects. This is useful for setting up local development vs CI environments. For example, you could have a profile that sets your local proxy settings.
mvn --encrypt-password to generate an obfuscated string stored in settings-security.xml. Or better, pass credentials via CI secrets and command-line flags (-Dnexus.password=...).Dependency Convergence — The Silent Classpath Nightmare
You think your POM is clean. You pinned a version. But Maven resolved three different versions of the same library through transitive dependencies, and now your app silently fails because the wrong version's class ended up on the classpath first. This is dependency convergence, and it's why your integration tests pass locally but explode in production.
Maven doesn't merge conflicts. It picks the 'nearest' dependency based on the shortest path in the dependency tree. That 'nearest' may be ancient with a known CVE, or incompatible with your code. The fix isn't hoping Maven guesses right. You need to enforce convergence using the maven-enforcer-plugin with the dependencyConvergence rule. This plugin will flat-out reject your build if Maven detects multiple versions of the same artifact in the tree.
Running mvn enforcer:enforce after compilation will surface the exact artifact and version paths. Then you either explicitly exclude the wrong transitive dependency or lift the correct version into your direct dependencies with an <exclusion> to shorten the path. Standardise your version management with a BOM first, then let the enforcer slap you when you screw up.
Exclusions Are Not Optional — Why Logging Hell Is Your Fault
Every mature project has a logging framework. SLF4J. Logback. Log4j 2. And then some cowboy dependency pulls in Log4j 1.x and another pulls in commons-logging. Now you have SLF4J bindings for three different backends, the JUL adapter is throwing bridge exceptions, and your production logs are silent because the app loaded the wrong Logback configuration.
This is logging hell. It's not a design problem. It's a resolver problem. You fix it by explicitly excluding every transitive logging dependency and replacing them with a single SLF4J-to-Logback bridge. Use mvn dependency:tree -Dincludes=log to find the offenders, then add <exclusions> in your direct dependency declarations. Do not let your POM become a transitive dumpster fire.
The pattern is simple: declare SLF4J API, Logback core, and Logback classic as direct dependencies. Then for every library that pulls in log4j, commons-logging, or jul-to-slf4j, exclude them. Your build will fail if you miss one. Use the enforcer plugin's bannedDependencies rule to make sure no one adds a transitive logging framework ever again. Run mvn dependency:tree and stare at the output until every log entry points to Logback.
bannedDependencies to the enforcer plugin to reject log4j, commons-logging, and all SLF4J bindings except Logback. One config, zero surprises. Your future self will thank you when the auditor asks why there's no Log4Shell in your dependency tree.Maven Commands and Options — Know What You're Typing
Every mvn command is a conversation with the build engine. Understand what you're asking for. 'mvn clean verify' tells Maven to nuke the output directory, recompile, run unit tests, and check integration constraints. It does not deploy. It does not build a distribution. That's 'mvn deploy'. Mixing these up when your CI pipeline runs a release asks for Production downtime.
Options control the conversation. '-o' runs offline — no network fetch, no falling over when your VPN drops. '-Pproduction' activates the production profile, loading real DB credentials and disabling test coverage. '-DskipTests' is a surgeon's knife: skip unit tests for a fast rebuild. But you'd better have run them before commit.
Senior devs memorize the dozen commands they use daily and ignore the rest. 'mvn dependency:tree' debugs classpath hell. 'mvn versions:display-dependency-updates' tells you what's rotting. Stop typing blind. Read the output. Every line is a signal for a problem you introduced.
Maven Lifecycle Phases Are Your Deployment Stages
Maven has three built-in lifecycles: clean, default, and site. Clean removes junk. Default compiles, tests, packages, and installs. Site generates documentation. You never type a lifecycle name—you type a phase. 'mvn compile' runs phases before compile, then stops. 'mvn test' runs compile, then test. The chain is linear and deterministic. Violate it and you lose repeatability.
Phases map to CI pipeline stages. validate runs pre-checks. compile produces bytecode. test catches bugs. package creates a JAR. verify runs integration checks. install drops into local repo. deploy pushes to remote. Your CI should run 'mvn verify' then conditionally 'mvn deploy' only on master. Mixing phases means deploying untested code.
Senior devs treat phases as immutable gates. Every phase failed tells you exactly where you broke the build. If test fails, you don't reach package. That's the entire point. Stop fighting the lifecycle—it's your safety net, not your cage.
Dependency Mediation — How Maven Picks the Winner in Version Conflicts
When multiple dependencies pull different versions of the same library, Maven doesn't merge or fail — it picks one. The rule is simple: nearest definition wins. That means the version declared closest to your project in the dependency tree takes priority. If A depends on B v1.0 and C depends on B v2.0, the version depends on which dependency (A or C) is declared at the shortest path from your POM. This is Maven's dependency mediation, and it's silent. You won't get a warning unless you explicitly check. The consequence? Your runtime may use a version nobody intended. A transitive deep dependency could override your explicit version if it sits closer in the tree. To see what actually wins, run 'mvn dependency:tree -Dverbose'. The output shows which artifacts were selected and which were omitted. Never assume your declared version is the one Maven uses — verify.
Exclusions — How to Remove Unwanted Transitive Dependencies with Precision
Transitive dependencies are useful until they bring in logging frameworks, outdated libraries, or security vulnerabilities. Exclusions let you strip specific dependencies from a declared dependency without modifying the dependency's own POM. The syntax lives inside the <dependency> block: <exclusions><exclusion><groupId>...</groupId><artifactId>...</artifactId></exclusion></exclusions>. This tells Maven "resolve this dependency, but pretend it never declared the excluded artifact." Why does this matter? Because logging hell — where multiple SLF4J bindings or Logback copies collide — is the number one classpath conflict in Java projects. Without exclusions, every library that transitively pulls commons-logging or log4j creates duplicates. The fix is to exclude the transitive logging dependency from every dependency that brings it in, then declare only one logging implementation explicitly. Exclusions are not optional — they are the surgical instrument for classpath hygiene. Apply them at the dependency level, not in dependency management, to keep exclusions scoped to the modules that need them.
The Guava 32 Nightmare — A Transitive Version Conflict That Took Down Production
- Explicit dependencies in <dependencies> only override transitive versions at depth 1 — not deeper transitive paths that use different artifactIds.
- Always use <dependencyManagement> to enforce versions across all transitive paths, especially for widely used libraries like Guava.
- Run mvn dependency:tree -Dverbose before every major release to catch hidden version conflicts.
- Test in a production-like classpath — test environments often have fewer dependencies, so conflicts may not surface.
mvn dependency:tree -Dincludes=com.google.guava:*mvn dependency:tree -DverboseKey takeaways
Common mistakes to avoid
5 patternsUsing <dependencies> instead of <dependencyManagement> for version control in multi-module projects
Forgetting to add <exclusions> when swapping a transitive library
Treating nearest-wins as 'best version wins' and not checking the dependency tree
Committing SNAPSHOT dependencies in a release artifact
Not using a BOM for shared dependency versions across multiple projects
Interview Questions on This Topic
What is the difference between
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Build Tools. Mark it forged?
18 min read · try the examples if you haven't