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
Lesson 4 of 7
1d streakThe 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.
<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>| Element | What it means | Common mistake |
|---|---|---|
modelVersion | The POM model schema Maven reads | Treating it as the installed Maven version |
| GAV coordinates | Uniquely identify the project and published artifact | Changing artifactId when only the display name should change |
packaging | The primary artifact type; jar is the default | Assuming packaging alone configures an executable Spring Boot JAR |
properties | Named values reused by POM elements and plugins | Assuming every property changes behavior automatically |
dependencies | Libraries this project declares and their scopes | Adding a version when a parent/BOM already manages it |
build/plugins | Tools that compile, test, package, or verify | Confusing 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.
| Scope | Available when | Typical use |
|---|---|---|
| compile (default) | Compile, test, and runtime | Application API used by main code |
| runtime | Test and runtime, not main compilation | Database driver loaded at runtime |
| test | Test compile and test execution only | JUnit and test helpers |
| provided | Compile and test, supplied by runtime | Container-owned API in specialized deployments |
Spring Boot manages a compatible set
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.
<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>| Concept | What it controls | What it does not do |
|---|---|---|
| Parent POM | Inherited properties, plugin management, and dependency management | It does not put the parent artifact on the runtime classpath |
| Spring Boot parent version | The Boot release whose defaults and managed versions are used | It does not mean every arbitrary library version is managed |
BOM (spring-boot-dependencies) | Versions for dependencies covered by Boot | It does not add dependencies or manage Maven plugin versions |
dependencies | The libraries actually requested by this project | It does not select a library merely because a BOM lists it |
dependencyManagement | Defaults/constraints for a declared dependency | It 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.
<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>./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 -DforceStdoutPrefer the managed set
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.
./mvnw --version
./mvnw clean verify
./mvnw dependency:tree
jar tf target/task-api-0.0.1-SNAPSHOT.jar | head| Command | What it proves | Important output |
|---|---|---|
./mvnw clean | Previous generated output is removed | A fresh target/ on the next build |
./mvnw test | Main and test code compile and tests run | Surefire reports |
./mvnw package | Tests pass and an archive is built | JAR under target/ |
./mvnw verify | All configured verification checks run | Build suitable for CI |
./mvnw dependency:tree | The resolved dependency graph is visible | Chosen 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.
[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)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.
./mvnw dependency:tree
./mvnw dependency:tree -Dincludes=org.junit.jupiter
./mvnw help:effective-pom
./mvnw clean verifyActive 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.
<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>Guided LabBuild & Verify Step by Step
A plain Java 21 Maven project containing pom.xml, Maven Wrapper files, TaskReadiness.java, and one JUnit test.
- 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.
- 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.
- 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.
- 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
[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
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
Key Takeaway
The classpath is decided at build time. Dependencies, scopes, and conflict mediation determine what the JVM actually sees at runtime — most mysterious NoSuchMethodError failures are build-tool misunderstandings.
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
Lesson Primary References
Supplementary Module Resources