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.
Learn Maven & Gradle in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
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
🧱 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.
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 + version name your artifact, e.g. com.mycompany:my-app:1.0.0 .
- Dependencies — each one is another GAV. A scope controls where it appears: compile (default, everywhere) or test (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.
🎯 Your Turn #1 — Finish the pom.xml
Fill in the artifactId, version, and the dependency scope. The expected result is 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.
🎯 Your Turn #2 — Finish the build.gradle
Add the repository and the two dependency configurations. The expected result is 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.
Common Errors
- ❌ Version conflict ( NoSuchMethodError at runtime): two libraries dragged in different versions of the same transitive dependency, so the wrong one won. Diagnose with mvn 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> or 1.+ means "whatever is newest", so a new release silently breaks your build. Always pin an exact version like 2.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 the plugins {} block (Gradle), and give each one a version.
- ❌ Could not resolve dependency : a typo in the GAV coordinates or a missing mavenCentral() repository. Double-check the exact coordinates on search.maven.org and confirm a repository is declared.
Pro Tips
- 💡 Generate new projects from start.spring.io or mvn 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)
./gradlew build
Run tests
./gradlew test
Install to local repo
./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
🎉 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).
Practice quiz
What are the three GAV coordinates that uniquely identify an artifact?
- name, type, scope
- repo, branch, tag
- group, artifact, version
- id, key, value
Answer: group, artifact, version. groupId (who), artifactId (what), and version (which release) form coordinates like com.mycompany:my-app:1.0.0.
What is the main difference between Maven and Gradle config files?
- Maven uses XML (pom.xml), Gradle uses a script (build.gradle)
- Maven uses code, Gradle uses XML
- Both use YAML
- Both use JSON
Answer: Maven uses XML (pom.xml), Gradle uses a script (build.gradle). Maven configures in XML (pom.xml) with strict conventions; Gradle uses a Groovy/Kotlin script, more concise and flexible.
Because each Maven phase runs every phase before it, what does 'mvn package' do first?
- Nothing else
- Only compile
- install and deploy
- validate, compile, then test
Answer: validate, compile, then test. Lifecycle phases run in order, so package automatically validates, compiles, and tests before packaging.
What is a transitive dependency?
- A dependency you list twice
- A dependency of your dependency that you never listed yourself
- A test-only dependency
- A dependency on the JDK
Answer: A dependency of your dependency that you never listed yourself. jackson-databind pulls in jackson-core and jackson-annotations automatically — those are transitive dependencies.
Which Maven scope puts a dependency only on the test classpath?
- test
- compile
- runtime
- provided
Answer: test. The test scope keeps a dependency (like JUnit) off the main classpath and out of the shipped JAR.
What is Gradle's equivalent of Maven's compile scope?
- testImplementation
- runtimeOnly
- implementation
- compileOnly
Answer: implementation. implementation '...' is a one-line compile dependency, replacing Maven's seven-line XML block.
Why should you always commit and use the wrapper (./gradlew or mvnw)?
- It builds faster
- It pins the exact build-tool version so everyone builds identically
- It is required by Java 21
- It skips tests
Answer: It pins the exact build-tool version so everyone builds identically. The wrapper pins the tool version for the whole team, avoiding 'works on my machine' build differences.
What does a -SNAPSHOT version mean?
- An immutable release
- A backup copy
- A security patch
- An in-progress build that can change at any time and is re-downloaded
Answer: An in-progress build that can change at any time and is re-downloaded. A -SNAPSHOT is a mutable, in-progress build; a release version like 1.2.0 is immutable for reproducible builds.
Why should you pin an exact dependency version like 2.17.0?
- It downloads faster
- So today's build is identical to next year's — LATEST or ranges can silently break it
- It uses less disk
- Maven requires it
Answer: So today's build is identical to next year's — LATEST or ranges can silently break it. Pinning an exact version makes builds reproducible; LATEST or open ranges let a new release break your build.
Which command prints the resolved transitive dependency tree in Gradle?
- ./gradlew build
- ./gradlew clean
- ./gradlew dependencies
- ./gradlew run
Answer: ./gradlew dependencies. ./gradlew dependencies prints the resolved graph (Maven's equivalent is mvn dependency:tree).
Continue this course
- Previous: Previous Lesson
- Next: Next Lesson