Home

Maven Basic Principles and Best Practices

Li

Li Wei

December 26, 20258 min read

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 NoClassDefFoundError unless 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:

  • dependencyManagement only 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 tree goal of maven-dependency-plugin prints the project’s dependency tree, while the copy-dependencies goal 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: cleandependency:treepackage.

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 pluginManagement constraints to plugin configurations.
  • Resolve imported POMs and use their dependencyManagement to constrain direct dependencies.
  • 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 -pl list simultaneously
  • -amd,--also-make-dependents: build modules that are depended on by the -pl list 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.

Keep reading

More related articles from DriftSeas.