Lesson 38 • Advanced
Maven & Gradle
No serious Java project is built by hand. By the end of this lesson you'll write a pom.xml and a build.gradle, add pinned dependencies, run the Maven lifecycle, and know exactly when to reach for Maven versus Gradle.
Before You Start
You should be comfortable with Unit Testing (build tools run your tests) and basic OOP project structure. A little XML familiarity helps for Maven, but it is not required.
What You'll Learn
- ✓You'll be able to write a pom.xml with GAV coordinates and dependencies
- ✓You'll be able to run the Maven lifecycle: validate, compile, test, package, install
- ✓You'll be able to write a build.gradle with dependencies and tasks
- ✓You'll be able to add dependencies and pin exact versions safely
- ✓You'll be able to read a transitive dependency tree and fix version conflicts
- ✓You'll be able to choose Maven or Gradle for a given project
🧱 The Big Idea — A Build Tool Is Your Project's Recipe
A build tool turns your source code into a runnable, shippable artifact. It fetches the libraries you need, compiles your code, runs your tests, and packages the result into a JAR — every time, the same way, on every machine.
💡 Analogy: A build tool is like a recipe with an automatic grocery service. The recipe (your pom.xml or build.gradle) lists the ingredients (dependencies) and the steps (compile, test, package). The tool does the shopping for you — it downloads each ingredient and the ingredients those ingredients need (transitive dependencies) — then cooks the dish in the right order. Maven hands you a fixed recipe template; Gradle lets you write your own.
Before build tools, developers downloaded JARs by hand and fought "it works on my machine" bugs. Maven (2004) and Gradle (2012) ended that: declare what you need, and the tool produces an identical build for everyone.
1️⃣ Maven — Create, Build, and the Lifecycle
Maven is built on convention over configuration: put your code in src/main/java and your tests in src/test/java, and Maven just knows what to do. You drive it through lifecycle phases that always run in order.
| Phase | Command | What it does |
|---|---|---|
| validate | mvn validate | Check the project is correct |
| compile | mvn compile | Compile source to target/classes |
| test | mvn test | Run the unit tests |
| package | mvn package | Bundle into a JAR/WAR |
| install | mvn install | Copy the JAR to your local ~/.m2 repo |
The key rule: each phase runs every phase before it. So mvn package automatically validates, compiles, and tests first. Read the worked output below — you can see all four phases run from one command.
# Generate a new project with the standard directory layout.
mvn archetype:generate \
-DgroupId=com.mycompany \
-DartifactId=my-app \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
cd my-app
# The layout Maven creates AND expects (convention over configuration):
# src/main/java/ your application source code
# src/main/resources/ config files (application.properties)
# src/test/java/ your test code
# target/ build output (compiled classes + the JAR)
# pom.xml the Project Object Model — deps + build config
# Run the build. 'package' runs every phase BEFORE it, in order:
# validate -> compile -> test -> package
mvn package[INFO] --- compiler:3.13.0:compile (default-compile) ---
[INFO] Compiling 1 source file to /my-app/target/classes
[INFO] --- surefire:3.2.5:test (default-test) ---
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.4.1:jar (default-jar) ---
[INFO] Building jar: /my-app/target/my-app-1.0.0.jar
[INFO] BUILD SUCCESS
[INFO] Total time: 4.218 smvn package output. Run them in your own shell after installing a JDK and Maven; one command runs the whole compile -> test -> package chain.2️⃣ The pom.xml — Coordinates, Scopes, and Plugins
The pom.xml (Project Object Model) is Maven's recipe. Three things make it work:
- GAV coordinates —
groupId+artifactId+versionname your artifact, e.g.com.mycompany:my-app:1.0.0. - Dependencies — each one is another GAV. A
scopecontrols where it appears:compile(default, everywhere) ortest(test classpath only). - Plugins — these add real work to the lifecycle. The shade plugin, for example, builds a "fat JAR" that bundles your dependencies inside.
2.17.0. Never use LATEST or an open range — a pinned version makes today's build identical to next year's.<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<!-- GAV coordinates: the three values that uniquely name your artifact. -->
<groupId>com.mycompany</groupId> <!-- who: reverse-domain namespace -->
<artifactId>my-app</artifactId> <!-- what: the project name -->
<version>1.0.0</version> <!-- which release -->
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- compile scope (the default): on the classpath everywhere. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version> <!-- always PIN an exact version -->
</dependency>
<!-- test scope: only on the test classpath, not shipped in the JAR. -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Plugins add steps to the lifecycle. Shade builds a "fat JAR". -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
</project>pom.xml, not runnable code. Save it at the project root and build with mvn package. Note the test scope and the pinned versions.🎯 Your Turn #1 — Finish the pom.xml
Fill in the artifactId, version, and the dependency scope. The expected result is in the comments.
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<!-- 🎯 YOUR TURN — fill in the blanks marked with ___ -->
<!-- 1) Give this artifact its GAV coordinates. -->
<groupId>com.example</groupId>
<artifactId>___</artifactId> <!-- 👉 name it "todo-app" -->
<version>___</version> <!-- 👉 use "1.0.0" -->
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>___</scope> <!-- 👉 tests only: use "test" -->
</dependency>
</dependencies>
</project>
<!-- ✅ Expected: 'mvn validate' prints BUILD SUCCESS with -->
<!-- artifactId = todo-app, version = 1.0.0, JUnit on the TEST classpath. -->___, save as pom.xml, and run mvn validate. Check your work against the expected result in the comments.3️⃣ Gradle — The Same Job, Written in Code
Gradle does everything Maven does, but the recipe is a real script (build.gradle in Groovy, or build.gradle.kts in Kotlin) instead of XML. The payoff is brevity and speed.
plugins { ... } — turn on capabilities (the java plugin adds compile/test/jar tasks).
repositories { mavenCentral() } — where dependencies are downloaded from.
implementation '...' — a compile dependency, written on one line (Maven's compile scope).
testImplementation '...' — a test-only dependency (Maven's test scope).
tasks — the unit of work Gradle runs (build, test, run, or your own).
A single implementation line replaces a seven-line XML block. Gradle is also typically 2-10x faster on repeat builds because it caches task outputs and keeps a daemon warm.
// build.gradle (Groovy DSL) — code, not XML. Far less ceremony.
plugins {
id 'java' // adds compile, test, jar tasks
id 'application' // adds the 'run' task
}
group = 'com.mycompany'
version = '1.0.0'
repositories {
mavenCentral() // where dependencies are downloaded from
}
dependencies {
// implementation = compile scope. ONE line vs 7 of XML.
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
// testImplementation = test scope (only on the test classpath).
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
application {
mainClass = 'com.mycompany.App' // entry point for './gradlew run'
}
tasks.named('test') {
useJUnitPlatform() // run tests with the JUnit 5 engine
}build.gradle (Groovy DSL), not runnable code. Save it at the project root and build with ./gradlew build. Each implementation line is one dependency.# Always use the wrapper (./gradlew) committed in the repo, NOT a global
# 'gradle' install — the wrapper pins the exact Gradle version for everyone.
./gradlew build # compile + test + package (= mvn package)
./gradlew test # run the tests (= mvn test)
./gradlew run # run the application (= mvn exec:java)
./gradlew clean # delete build output (= mvn clean)
./gradlew dependencies # print the resolved graph (= mvn dependency:tree)> Task :compileJava
> Task :test
BUILD SUCCESSFUL in 1s
7 actionable tasks: 4 executed, 3 up-to-date
# ./gradlew dependencies shows TRANSITIVE deps you never listed:
compileClasspath
\--- com.fasterxml.jackson.core:jackson-databind:2.17.0
+--- com.fasterxml.jackson.core:jackson-annotations:2.17.0
\--- com.fasterxml.jackson.core:jackson-core:2.17.0./gradlew). The output shows the build succeeding and the transitive dependencies that one declared dependency pulled in.🎯 Your Turn #2 — Finish the build.gradle
Add the repository and the two dependency configurations. The expected result is in the comments.
// build.gradle
// 🎯 YOUR TURN — fill in the blanks marked with ___
plugins {
id 'java'
}
repositories {
___() // 👉 the standard public repo: mavenCentral
}
dependencies {
// A normal compile-time library:
___ 'org.apache.commons:commons-lang3:3.14.0' // 👉 use: implementation
// A test-only library:
___ 'org.junit.jupiter:junit-jupiter:5.10.2' // 👉 use: testImplementation
}
// ✅ Expected: './gradlew build' downloads both jars from Maven Central and
// prints BUILD SUCCESSFUL. commons-lang3 is on the main classpath;
// junit-jupiter is on the test classpath only.___, save as build.gradle, and run ./gradlew build. Check your work against the expected result in the comments.4️⃣ Dependency Management & Transitive Dependencies
You rarely depend on just one library. When you add jackson-databind, it needs jackson-core and jackson-annotations — so the build downloads those too. Those are transitive dependencies: dependencies of your dependencies.
Sometimes two libraries pull in different versions of the same transitive dependency — a version conflict. Maven resolves it by "nearest wins" (the version closest to your project in the tree). To see and debug the full graph, use mvn dependency:tree or ./gradlew dependencies, then pin the version you want explicitly to force a single, predictable answer.
Mini-Challenge — Add a Library Yourself
No blanks this time — just the brief. Write the dependency lines from scratch, then verify with the dependency report.
// 🎯 MINI-CHALLENGE: add a JSON library to a Gradle build
//
// Starting from a build.gradle that already has the 'java' plugin and
// mavenCentral():
// 1. Add Google's Gson as a COMPILE dependency.
// coordinates -> com.google.code.gson:gson:2.11.0
// 2. Add JUnit 5 as a TEST-ONLY dependency.
// coordinates -> org.junit.jupiter:junit-jupiter:5.10.2
// 3. Run './gradlew dependencies' to confirm Gson resolved.
//
// ✅ Expected: BUILD SUCCESSFUL, and the dependency report lists
// com.google.code.gson:gson:2.11.0 on the compileClasspath.
// your build.gradle dependencies { } block heredependencies { ... } block yourself, then run ./gradlew dependencies to confirm Gson resolved. Compare against the expected result in the comments.Common Errors
- ❌ Version conflict (
NoSuchMethodErrorat runtime): two libraries dragged in different versions of the same transitive dependency, so the wrong one won. Diagnose withmvn dependency:tree, then pin the version you want in your own<dependencies>/dependencies {}block to override it. - ❌ Shipping against a
-SNAPSHOT: a SNAPSHOT can change under you, so a build that worked yesterday breaks today. Depend on SNAPSHOTs only during active development; for anything you release, pin a real release version. - ❌ Not pinning versions (
LATEST/ open ranges):<version>LATEST</version>or1.+means "whatever is newest", so a new release silently breaks your build. Always pin an exact version like2.17.0. - ❌ Plugin misconfiguration (
No plugin found for prefix 'X'or skipped phase): a plugin in the wrong place or with a missing version won't bind to the lifecycle. Put plugins inside<build><plugins>(Maven) or theplugins {}block (Gradle), and give each one a version. - ❌
Could not resolve dependency: a typo in the GAV coordinates or a missingmavenCentral()repository. Double-check the exact coordinates onsearch.maven.organd confirm a repository is declared.
Pro Tips
- 💡 Generate new projects from
start.spring.ioormvn archetype:generate— never hand-write the whole file. - 💡 Always commit the wrapper (
mvnw/gradlew) so everyone builds with the same tool version. - 💡 Use a BOM (Bill of Materials) to manage versions of related libraries in one place, so they never drift apart.
- 💡 Add
target/,build/, and.idea/to.gitignore— build output is regenerated, never committed.
📋 Quick Reference — Maven vs Gradle
| Task | Maven | Gradle |
|---|---|---|
| Config file | pom.xml (XML) | build.gradle (code) |
| Build (compile+test+jar) | mvn package | ./gradlew build |
| Run tests | mvn test | ./gradlew test |
| Install to local repo | mvn install | ./gradlew publishToMavenLocal |
| Clean output | mvn clean | ./gradlew clean |
| Compile dependency | <scope>compile</scope> | implementation '...' |
| Test dependency | <scope>test</scope> | testImplementation '...' |
| Dependency tree | mvn dependency:tree | ./gradlew dependencies |
Frequently Asked Questions
What is the difference between Maven and Gradle?
Both are build tools that compile, test, package, and manage dependencies for Java projects. Maven configures everything in an XML file (pom.xml) and favours strict conventions, so projects look the same everywhere. Gradle configures the build in real code (a Groovy or Kotlin script), which is more concise and flexible, and it is usually 2-10x faster on repeat builds thanks to incremental compilation, a build cache, and a long-running daemon.
What are groupId, artifactId, and version (GAV coordinates)?
They are the three values that uniquely identify any artifact in a repository like Maven Central. groupId is your organisation's reverse-domain namespace (com.mycompany), artifactId is the project name (my-app), and version is the release (1.0.0). Together they form a coordinate such as com.mycompany:my-app:1.0.0 that any other build can depend on.
What is a transitive dependency?
It is a dependency of your dependency that you never listed yourself. If you depend on jackson-databind, it pulls in jackson-core and jackson-annotations automatically, and those come in too. Both Maven and Gradle resolve this whole graph for you. Run 'mvn dependency:tree' or './gradlew dependencies' to see every transitive jar your build is actually using.
What does a SNAPSHOT version mean, and how is it different from a release?
A version ending in -SNAPSHOT (e.g. 1.2.0-SNAPSHOT) is an in-progress build that can change at any time, so the tool re-downloads the latest copy. A release version (1.2.0) is immutable: once published it never changes, so builds are reproducible. Depend on SNAPSHOTs only inside your own active development; never ship a release that depends on someone else's SNAPSHOT.
Should I choose Maven or Gradle for a new project?
Pick Maven when you want simplicity, stability, and a setup any Java developer instantly recognises — it is the enterprise default and great for straightforward libraries and services. Pick Gradle when build speed matters, when you have many modules, or when you need custom build logic — it is the standard for Android and a popular choice for new Spring Boot apps. Either way, generate the project from start.spring.io or 'mvn archetype:generate' rather than writing it by hand.
🎉 Lesson Complete!
You can now write a pom.xml with GAV coordinates and scoped dependencies, run the Maven lifecycle (validate → compile → test → package → install), write a build.gradle with dependencies and tasks, read a transitive dependency tree, fix version conflicts, and choose Maven or Gradle for the job. That is the build-tool literacy every Java team expects.
Next up: Modular Java — the Java Platform Module System (JPMS).
Sign up for free to track which lessons you've completed and get learning reminders.