Maven Basic Principles and Best Practices
Li Wei
Title: Maven Basics and Best Practices
Concepts and Principles
Coordinates
Each artifact can be uniquely identified by its coordinates.
- groupId: usually the name of the project to which the build belongs
- artifactId: a Maven module within the project
- version: version number
- packaging: packaging type, e.g.,
jar,war,pom - classifier: auxiliary artifact, e.g., sources
The file name of a project artifact corresponds to its coordinates, usually artifactId-version[-classifier].packaging.
Dependencies
The definition used when importing a target artifact into a project.
- groupId: usually the name of the project to which the build belongs
- artifactId: a Maven module within the project
- version: version number
- type: corresponds to the project's coordinates
- scope: dependency scope
- optional: whether the dependency is optional
- exclusions: exclude transitive dependencies
For example:
Transitive Dependencies
Automatically bring in indirect dependencies, avoiding manual handling.
Transitive Dependencies and Dependency Mediation
Resolve the problem of multiple versions appearing in transitive dependencies. Different dependency‑management tools adopt different strategies; for instance, Go Modules use a method called Minimal Version Selection (MVS), which picks the highest version among the candidates.
Maven’s approach is called the nearest definition principle:
- Choose the version that is closest in the dependency path.
- If the paths are equally distant, choose the version that was declared first.
Example: infra-framework-rpc-core depends on guava and consul-client, and these two dependencies transitively bring in different versions of jsr305. When guava is declared before consul-client, the effective jsr305 version is 3.0.2; when guava is declared after consul-client, the effective version is 1.3.9.
Transitive Dependencies and Exclusions
The importing side can actively exclude indirect dependencies.
Exclusions are mainly used in the following scenarios:
- Excluding problematic dependencies, e.g., two third‑party libraries containing classes with the same fully‑qualified name.
- Resolving conflicts between multiple dependencies, e.g., when both log4j and logback are present.
- Breaking circular dependencies in certain cases (though it is not recommended; extracting a common dependency is preferred).
Transitive Dependencies and Scopes
- compile: needed at compile time, runtime, and test time.
- provided: needed only at compile time, e.g.,
javax.servlet-api. - runtime: needed only at runtime, e.g.,
mysql-connector-java. - test: needed only when building and executing test code.
Transitive Dependencies and Optional Dependencies
The author of a library decides not to propagate a dependency.
Optional dependencies are useful in the following situations:
A library offers many features but only a subset is used, e.g., the various conditional capabilities of Spring Boot.
In special cases to resolve circular dependencies.
Example: A → B (A depends on B), B → A (B depends on A). Maven’s default transitive dependency mechanism would create a cycle that cannot be resolved.
When B declares its dependency on A as **optional**: B internally depends on A but marks the dependency as “optional”. Consequently, B’s dependency on A does not trigger reverse propagation (A will not automatically depend on B because B depends on it), breaking the cycle.
Note: Suppose project C wants to depend on B. Because B’s dependency on A is optional, Maven will not automatically pull A into C’s dependency graph. If C’s code uses A (e.g., B calls a method in A and C indirectly calls A through B), C will fail with a
NoClassDefFoundErrorunless it explicitly declares a dependency on A. This is the “drawback” of this technique—it adds configuration cost to downstream projects.
Transitive Dependencies and Dependency Management
dependencyManagement: a mechanism for centrally managing dependency information.
Two main purposes:
- Simplify dependency declarations; modules only need to specify groupId and artifactId.
- Most importantly, constrain the versions of transitive dependencies (has no effect on direct dependencies).
Note:
dependencyManagementonly manages versions; it does not actually bring in the dependencies. It must be distinguished from the<dependencies>section.- Apart from groupId and artifactId, all other constraints are optional; if no version is specified, normal dependency mediation will apply.
Inheritance
Inherit properties, dependencies, dependencyManagement, build, etc., from a parent POM, offering stronger constraints than a BOM.
Common practices:
- Multi‑module parent‑child projects, split into modules according to code responsibilities.
- Super POM (root pom): use a single parent POM to handle dependency management, plugin management, unified Checkstyle configuration, Enforcer rules, and so on.
Lifecycle and Plugins
Lifecycle
A lifecycle (e.g., default, clean, site) abstracts and unifies the build process, breaking execution logic into multiple phases.
A lifecycle is a logical concept; it does not perform actions on project files itself.
Each lifecycle contains a series of ordered phases; later phases require earlier ones to have run.
Plugins are bound to specific phases and perform the actual build tasks.
There are three independent lifecycles with no inherent ordering between them:
- clean: cleans the project (phases
pre-clean,clean,post-clean) - default: builds the project (phases
validate,initialize, …compile,package,verify,install,deploy, etc.) - site: generates the project site
Phase and Plugin
Plugin: the component that actually executes build tasks.
- Goal: each plugin has multiple goals that perform different functions. For example, the
treegoal ofmaven-dependency-pluginprints the project’s dependency tree, while thecopy-dependenciesgoal copies dependency JARs to a designated directory.
Plugin binding: a plugin goal is associated with a lifecycle phase and will be executed when that phase runs.
- Built‑in binding: default bindings provided by Maven.
- Custom binding: specify the phase in the plugin’s
<executions>configuration.
Command‑line execution: run a plugin goal directly from the command line.
Execution Process
1. Process command‑line arguments – format: mvn [options] [<goal(s)>] [<phase(s)>]
Execution order: clean → dependency:tree → package.
2. Parse POM and build the reactor
Parse the POM to obtain the effective POM:
- Parse the current POM to collect
dependencies,dependencyManagement, etc. - Recursively parse parent POMs.
- Merge parent‑child configurations from farthest to nearest, mainly using a “put‑if‑absent” strategy to assemble inheritance.
- Apply
pluginManagementconstraints to plugin configurations. - Resolve imported POMs and use their
dependencyManagementto constrain directdependencies. - The result is the final effective POM.
Module build order:
- Traverse
<modules>. - Build a DAG (directed acyclic graph).
- Perform a topological sort to obtain the reactor order.
3. Compute execution plan and run goals
Each module runs the lifecycle and plugins specified by the command‑line arguments.
4. Resolve the dependency tree
The dependency tree is a DAG; it is traversed with DFS (see mvn dependency:tree -Dverbose).
Dependency mediation follows the nearest‑definition rule (see mvn dependency:tree).
Dependency Exclusions
During DFS traversal, a cache keyed by GA + Exclusion is used. Different exclusion sets can cause repeated traversals and caching.
Practices
Dependency Management
Excluding dependencies is not the best way to manage them.
Transitive dependencies are inherently uncertain:
- If a dependency is a snapshot version, any change to its own dependencies will alter the dependent project’s dependency tree.
- Multiple transitive dependencies may be related; the nearest‑definition mediation can easily lead to binary incompatibilities (e.g., some middleware pulling in version 2.x of a library while others pull in 3.x).
Other Tips
IDEA Maven Helper Plugin
Quickly locate dependency conflicts and mediation results. In the screenshot below, you can clearly see that the path to jsr305 3.0.2 is shorter.
IDEA Effective POM
Outputs the flattened result of parent POMs and BOMs.
mvnDebug – Debugging Maven
99.0-does-not-exist
Add a deliberately non‑existent version (e.g., 99.0-does-not-exist) to Nexus and reference it via a parent POM or BOM to prevent an unwanted transitive dependency from being introduced.
Trimming the Reactor
-pl,--projects: list of modules to build-am,--also-make: build modules that depend on the-pllist simultaneously-amd,--also-make-dependents: build modules that are depended on by the-pllist simultaneously
${revision}
Manage the version of a multi‑module parent‑child project.
Parallel Builds
(Details omitted for brevity)
Building Specific Modules by ArtifactId
In large multi‑module projects, you can quickly compile a particular submodule:
(Example omitted)
Slimming Down Dependencies
dependency:analyze uses bytecode analysis to determine how dependencies are actually used.
Originally written by Li Wei (李唯_) and published in Chinese on 后端技术栈全书 (Full-Stack Backend Engineering). Translated and adapted for DriftSeas with permission.