Foundation · Prepare

Maven & Project Basics

Read a POM from top to bottom, explain parent versus BOM version management, inspect scopes, and use the committed wrapper.

Primary source: Apache Maven: Introduction to the Dependency Mechanism & Mediation

50 min

Lesson 4 of 7

1d streak

The Architectural Question

Every Spring Boot feature arrives as a dependency; without a build-tool model, version conflicts and missing classes are unsolvable mysteries.

What this guide proves

  • pom.xml structure
  • Dependencies and scopes
  • Build lifecycle

Read a POM from top to bottom

A POM (Project Object Model) is declarative XML: it describes the project and its build inputs; it is not a shell script that runs from top to bottom. Maven combines this file with inherited parent settings, imported dependency management, plugin defaults, profiles, and repository metadata to create the effective POM that the build actually uses.

pom-anatomy.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <!-- The POM model schema, not the Maven version used to run the build -->
  <modelVersion>4.0.0</modelVersion>

  <!-- Project coordinates: group, artifact, and version (GAV) -->
  <groupId>com.example</groupId>
  <artifactId>task-api</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <!-- Reusable values; a property matters only where a plugin or element reads it -->
  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <junit.version>5.12.2</junit.version>
  </properties>

  <!-- Adds libraries to this project's classpath -->
  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <!-- Plugins perform build work such as compiling and testing -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.2</version>
      </plugin>
    </plugins>
  </build>
</project>
ElementWhat it meansCommon mistake
modelVersionThe POM model schema Maven readsTreating it as the installed Maven version
GAV coordinatesUniquely identify the project and published artifactChanging artifactId when only the display name should change
packagingThe primary artifact type; jar is the defaultAssuming packaging alone configures an executable Spring Boot JAR
propertiesNamed values reused by POM elements and pluginsAssuming every property changes behavior automatically
dependenciesLibraries this project declares and their scopesAdding a version when a parent/BOM already manages it
build/pluginsTools that compile, test, package, or verifyConfusing a plugin with a library dependency

The standard layout is part of this model: Maven compiles src/main/java, copies src/main/resources, compiles tests from src/test/java, and writes generated output under target/. A plugin can change these conventions, but changing them early makes every IDE, CI job, and example harder to follow.

Active Recall Checkpoint

Where should a library used only by tests be declared, and why?

Show answer & explanation

Under dependencies with test scope. It is available to test compilation and execution but is kept off the application compile and runtime classpaths.

Dependencies, scopes, and the resolved graph

A dependency coordinate points Maven to an artifact repository. That artifact may declare its own dependencies, producing a transitive graph. Maven selects versions using dependency mediation and dependency management; it does not load every version side by side. When runtime code reports NoSuchMethodError, inspect the resolved graph because compilation and runtime may have selected incompatible classpaths.

Each dependency entry contributes one declared edge to the graph. Its groupId and artifactId identify the artifact, version selects it unless dependency management supplies one, and scope controls which classpaths can see it.

ScopeAvailable whenTypical use
compile (default)Compile, test, and runtimeApplication API used by main code
runtimeTest and runtime, not main compilationDatabase driver loaded at runtime
testTest compile and test execution onlyJUnit and test helpers
providedCompile and test, supplied by runtimeContainer-owned API in specialized deployments

Spring Boot manages a compatible set

Later, the Spring Boot parent or BOM supplies versions for supported libraries. Avoid overriding those versions unless you understand why the managed set is insufficient.

Spring Boot parent and BOM: two ways to align versions

Spring Boot adds a curated dependency platform to the plain Maven model. The spring-boot-starter-parent parent is a convenience option: its version is the Spring Boot release, and it contributes Boot dependency management plus useful Maven defaults and plugin management. A parent is inherited configuration; it is not a dependency placed on the application classpath.

boot-parent-pom.xml
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.5.16</version>
  <relativePath/> <!-- resolve from repositories, not ../pom.xml -->
</parent>

<properties>
  <java.version>21</java.version>
</properties>

<dependencies>
  <!-- Version is intentionally omitted: Boot's managed set supplies it. -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
</dependencies>

<build>
  <plugins>
    <!-- The parent manages this plugin version; the declaration enables repackaging. -->
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>
ConceptWhat it controlsWhat it does not do
Parent POMInherited properties, plugin management, and dependency managementIt does not put the parent artifact on the runtime classpath
Spring Boot parent versionThe Boot release whose defaults and managed versions are usedIt does not mean every arbitrary library version is managed
BOM (spring-boot-dependencies)Versions for dependencies covered by BootIt does not add dependencies or manage Maven plugin versions
dependenciesThe libraries actually requested by this projectIt does not select a library merely because a BOM lists it
dependencyManagementDefaults/constraints for a declared dependencyIt does not make the dependency available by itself

A project that already needs another parent can import the Boot BOM instead. This keeps Boot library version alignment while leaving parent inheritance available for the project’s own parent. In that arrangement, configure and pin build-plugin versions explicitly because a dependency BOM manages libraries, not Maven plugins.

boot-bom.xml
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>3.5.16</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
inspect-managed-versions.sh
./mvnw help:effective-pom > effective-pom.xml
./mvnw dependency:tree -Dverbose
./mvnw dependency:tree -Dincludes=org.springframework
./mvnw help:evaluate -Dexpression=spring-boot.version -q -DforceStdout

Prefer the managed set

Omitting a covered dependency version is intentional. Override it only for a documented compatibility or security reason, then run the full test and integration-test path because the curated set was tested together.

Active Recall Checkpoint

Does importing spring-boot-dependencies add spring-boot-starter-web to the application?

Show answer & explanation

No. The BOM only supplies version metadata. The starter still needs a normal entry under dependencies.

Phases and plugin goals are different things

A lifecycle phase is a milestone. Invoking a later phase runs the preceding phases in order: verify includes compilation, test compilation, and tests before final verification. A plugin goal is a concrete task bound to a phase or invoked directly. dependency:tree is a goal; test and package are lifecycle phases. This distinction makes build output predictable.

Use the committed Maven Wrapper for these commands so the build tool is pinned in local development and CI. The wrapper pins Maven itself, not the JDK, dependency versions, or external services.

wrapper-commands.sh
./mvnw --version
./mvnw clean verify
./mvnw dependency:tree
jar tf target/task-api-0.0.1-SNAPSHOT.jar | head
CommandWhat it provesImportant output
./mvnw cleanPrevious generated output is removedA fresh target/ on the next build
./mvnw testMain and test code compile and tests runSurefire reports
./mvnw packageTests pass and an archive is builtJAR under target/
./mvnw verifyAll configured verification checks runBuild suitable for CI
./mvnw dependency:treeThe resolved dependency graph is visibleChosen versions and parents

Active Recall Checkpoint

If ./mvnw package runs the tests, should CI separately call test and then package?

Show answer & explanation

Usually no. Calling package already traverses test. Use one intentional terminal phase such as verify unless separate jobs or reports require another structure.

Break it deliberately: Transitive version collision

A classic production outage occurs when two transitive dependencies pull incompatible versions of the same core library, resulting in a runtime NoSuchMethodError.

dependency-tree-conflict.txt
[INFO] com.example:task-api:jar:0.0.1-SNAPSHOT
[INFO] +- com.lib.a:library-a:jar:1.0.0:compile
[INFO] |  \- com.fasterxml.jackson.core:jackson-databind:jar:2.14.0:compile
[INFO] \- com.lib.b:library-b:jar:2.0.0:compile
[INFO]    \- (com.fasterxml.jackson.core:jackson-databind:jar:2.18.2:compile - omitted for conflict with 2.14.0)
Terminal / Diagnostic Output
java.lang.NoSuchMethodError: 'com.fasterxml.jackson.databind.ObjectMapper.readerFor(java.lang.Class)'
    at com.lib.b.Parser.parse(Parser.java:42)
    at com.example.tasks.TaskService.load(TaskService.java:18)

Maven uses the Nearest-Definition Rule: the version closest to the root project wins. Because library-a happened to be declared first, the older 2.14.0 version won, starving library-b of methods introduced in 2.18.0.

The definitive solution is Maven <dependencyManagement> or importing the Spring Boot BOM (spring-boot-dependencies), which forces a single convergent version across the entire transitively resolved graph.

Active Recall Checkpoint

Why does Maven choose 2.14.0 over 2.18.2 even though 2.18.2 is newer?

Show answer & explanation

Maven does not automatically pick the highest version; it picks the nearest node in the tree hierarchy. You must use dependencyManagement to enforce the intended version.

Diagnose the classpath before changing versions

When a class is missing, first determine whether the dependency was never declared, excluded, placed in the wrong scope, or absent from the packaged artifact. When two paths bring the same library, dependency:tree -Dverbose reveals the selected and omitted versions. Changing a random version may hide the immediate symptom while breaking the compatibility set elsewhere.

diagnose-maven.sh
./mvnw dependency:tree
./mvnw dependency:tree -Dincludes=org.junit.jupiter
./mvnw help:effective-pom
./mvnw clean verify

Active Recall Checkpoint

Main code imports JUnit, but the dependency has test scope. Should you change JUnit to compile scope?

Show answer & explanation

No. Production code should not depend on the test framework. Move the code to the test source set or replace that dependency with an application abstraction.

The primary Module 0 path is now Maven end to end: pom.xml, the Maven Wrapper, target/ output, and Maven lifecycle commands. Gradle remains a useful alternative build tool, but comparing it belongs after the Maven model is stable rather than inside the core exercise.

Guided Hands-On Lab

Code Walkthrough

Create a Maven project, trace managed versions with help:effective-pom and dependency:tree, run clean verify, and inspect the resulting JAR.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>task-api</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <properties>
    <java.version>21</java.version>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>3.5.16</spring-boot.version>
  </properties>

  <!-- Enforces convergent versions across the entire transitive dependency tree -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <!-- Compile scope: Application code -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Runtime scope: Required for execution, hidden from main compilation -->
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <scope>runtime</scope>
    </dependency>

    <!-- Test scope: Hidden from main source compilation -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
    </plugins>
  </build>
</project>
The compiler release property pins Java 21 as a rigid compiler contract instead of an IDE-only preference.
dependencyManagement imports the Spring Boot BOM, eliminating transitive version collisions and NoSuchMethodError. This is the BOM-only alternative to the Spring Boot parent; unlike a parent, it does not manage Maven plugin versions.
Scoping PostgreSQL to runtime and JUnit to test prevents production code from accidentally compiling against test harnesses or driver internals.
Guided LabBuild & Verify Step by Step

A plain Java 21 Maven project containing pom.xml, Maven Wrapper files, TaskReadiness.java, and one JUnit test.

  1. 01

    Put each dependency in the right scope

    Keep application libraries in compile scope, the PostgreSQL driver in runtime scope, and JUnit in test scope.

    Check: Production code cannot import a test library, and the driver is still there when the application runs.

  2. 02

    Read the resolved graph

    Run dependency:tree and identify the direct dependency that introduced each important transitive artifact.

    Check: For each important transitive library you can name the direct dependency that pulled it in.

  3. 03

    Walk the lifecycle

    Run clean, test, and package deliberately, then inspect Surefire reports and the archive under target.

    Check: Tests run before packaging, and a failing test stops the build before the jar exists.

  4. 04

    Prove the scopes are isolated

    Import a JUnit class from main source code, watch compilation fail, then remove the import.

    Check: Test scope stays invisible to the main source set.

Command

./mvnw clean verify
./mvnw dependency:tree

Expected Output

BUILD SUCCESS
target/surefire-reports
target/task-api-0.0.1-SNAPSHOT.jar
Simulated terminal replay — matched to lesson verificationJDK 21 / Maven 3.9
spring-boot-lab@localhost:~
$./mvnw clean verify ./mvnw dependency:tree#failingCondition
[INFO] Scanning for projects...
[INFO] -------------------< com.example:order-service >-------------------
[INFO] Building order-service 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-surefire-plugin:3.2.5:test (default-test) @ order-service ---
[INFO] Running com.example.OrderServiceTest
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.284 s <<< FAILURE! -- in com.example.OrderServiceTest
[ERROR] com.example.OrderServiceTest.testOrderCreation -- Time elapsed: 0.041 s <<< ERROR!
java.lang.NullPointerException: Cannot invoke "com.example.OrderRepository.save(Object)" because "this.repository" is null
	at com.example.OrderService.createOrder(OrderService.java:18)
	at com.example.OrderServiceTest.testOrderCreation(OrderServiceTest.java:24)

[INFO] Results:
[INFO]
[ERROR] Errors:
[ERROR]   OrderServiceTest.testOrderCreation:24 » NullPointer Cannot invoke "com.example...
[INFO]
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test on project order-service: There are test failures.
[ERROR] -> [Help 1]
[ERROR] BUILD FAILURE
Exit Code: 1
BUILD FAILURE

Synthesis & Production Verification

Production Acceptance Criteria

  • Read coordinates, properties, dependencies, dependencyManagement, and plugins in a POM
  • Explain parent versus BOM and compile, runtime, test, and provided scopes
  • Run clean, compile, test, and package and name what each produces

Architectural & Technical Review

Core Interview Questions

Q01What does mvn package produce, and what runs before packaging?Show answer

It produces the distributable jar in target/, but only after compiling sources and running tests. A passing package therefore proves compile plus test health, not just packaging.

Q02What is the difference between the Spring Boot parent and BOM?Show answer

The starter parent is inherited configuration: its Boot version contributes dependency management, plugin management, and defaults. The BOM is imported dependency management: it supplies versions for covered libraries but does not add dependencies or manage Maven plugins. Use the BOM when another parent must be retained.

Q03How does Maven resolve conflicting versions of the same library?Show answer

Dependency mediation uses the nearest definition in the dependency tree; at equal depth, the first declaration wins. dependencyManagement can provide the selected version without adding the dependency itself. The effective POM and dependency tree reveal the actual result.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Two versions of the same library on the classpath — Transitive dependencies silently pull older copies. Inspect the dependency tree and manage versions explicitly instead of hoping.
  • Pitfall: Depending on a globally installed Maven version — Teammates and CI get different results unless the project pins its build tool. Prefer the checked-in Maven Wrapper.
  • Pitfall: Committing build output or ignoring the lockfile story — target/ artifacts and ad-hoc local jars make builds irreproducible. Keep sources and POMs canonical; let the tool generate the rest.

Primary Documentation & Specifications