Learn Java

Lesson 65Phase 8: Tools of the trade

Maven and Gradle: building real projects

Project layout, dependencies, the build lifecycle, and running tests from the command line with Maven or Gradle.

By Updated 50 min

  • Maven
  • pom.xml
  • dependency
  • lifecycle
  • Gradle
  • build.gradle

Every program in this course so far has been a few files that you compile with javac or run with one button. A real project is bigger. It has dozens or hundreds of classes, it uses libraries that other people wrote, and it has tests that must pass before anyone ships it. A build tool takes care of all of that from one short command, the same way on every computer. This lesson works through Maven step by step, then builds the same project with Gradle. Along the way you learn to read what both of them print, especially when a build fails.

By the end of this lesson
  • Explain what a build tool does that javac and java alone do not.
  • Lay out a project the standard way, with src/main/java, src/test/java and a target or build folder for the output.
  • Read a pom.xml: the project's coordinates, and the dependencies it downloads from Maven Central.
  • Run mvn compile, mvn test and mvn package, and say which lifecycle phases each one runs.
  • Read a failed build and tell a compile error, a failing test and a missing dependency apart.
  • Build the same project with Gradle, and explain why projects commit the mvnw and gradlew wrapper scripts.
From before
You have compiled and run programs with javac and java since your first Java program. In packages and imports you put classes in packages, compiled them into an out folder with -d, ran them with -cp, and met the NoClassDefFoundError that appears when a JAR is missing from the classpath. Testing with JUnit gave you a BankAccount with tests and a first look at mvn test, and Git and GitHub kept target/ and build/ out of your repository. This page puts those pieces together.

What a build tool is for#

Here is the bank account from the testing lesson, grown into a small project. The class lives in the package com.example.bank, a Mainclass uses it, and a test class checks it. The account also stores its owner's name. People type names in all sorts of ways, so the account tidies the name with a library: code that someone else wrote, tested and shared, delivered as a JAR file. One method of Apache Commons Text turns ada LOVELACE into Ada Lovelace.

src/main/java/com/example/bank/BankAccount.java
read along
1package com.example.bank;
2
3import org.apache.commons.text.WordUtils;
4
5public class BankAccount {
6 private final String owner;
7 private int balance;
8
9 public BankAccount(String owner, int openingBalance) {
10 this.owner = WordUtils.capitalizeFully(owner);
11 this.balance = openingBalance;
12 }
13
14 public void deposit(int amount) {
15 if (amount <= 0) {
16 throw new IllegalArgumentException("Deposit must be positive");
17 }
18 balance = balance + amount;
19 }
20
21 public void withdraw(int amount) {
22 balance = balance - amount;
23 }
24
25 public String getOwner() {
26 return owner;
27 }
28
29 public int getBalance() {
30 return balance;
31 }
32}
src/main/java/com/example/bank/Main.java
read along
1package com.example.bank;
2
3public class Main {
4 public static void main(String[] args) {
5 BankAccount account = new BankAccount("ada LOVELACE", 100);
6 account.deposit(50);
7 account.withdraw(30);
8 System.out.println(account.getOwner() + " has " + account.getBalance());
9 }
10}

The test class, BankAccountTest, has the deposit and withdraw tests from the testing lesson and one more that checks the owner's name comes out as Ada Lovelace. Try to build the program with the tools you already know. From the project folder:

Terminal
read along
1$ javac -d out src/main/java/com/example/bank/*.java
2src/main/java/com/example/bank/BankAccount.java:3: error: package org.apache.commons.text does not exist
3import org.apache.commons.text.WordUtils;
4 ^
5src/main/java/com/example/bank/BankAccount.java:10: error: cannot find symbol
6 this.owner = WordUtils.capitalizeFully(owner);
7 ^
8 symbol: variable WordUtils
9 location: class BankAccount
102 errors

javac has never heard of Commons Text. A library is not part of Java, so you fetch its JAR yourself and put it on the classpath. Download commons-text-1.15.0.jarfrom the library's website and try again:

Terminal
read along
1$ javac -d out -cp commons-text-1.15.0.jar src/main/java/com/example/bank/*.java
2$ java -cp out:commons-text-1.15.0.jar com.example.bank.Main
3Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/lang3/StringUtils
4 at org.apache.commons.text.WordUtils.capitalizeFully(WordUtils.java:250)
5 at org.apache.commons.text.WordUtils.capitalizeFully(WordUtils.java:221)
6 at com.example.bank.BankAccount.<init>(BankAccount.java:10)
7 at com.example.bank.Main.main(Main.java:5)
8Caused by: java.lang.ClassNotFoundException: org.apache.commons.lang3.StringUtils
9 at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
10 at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
11 ... 4 more

It compiles, and then the program crashes. Read the stack trace from the top: inside Commons Text, capitalizeFully needed a class called StringUtils from a second library, Apache Commons Lang, which you never asked for. Libraries are built on other libraries. Only after you track down the right version of that one as well does the program print its line:

Terminal
read along
1$ java -cp out:commons-text-1.15.0.jar:commons-lang3-3.20.0.jar com.example.bank.Main
2Ada Lovelace has 120

That was one library. The tests need JUnit, which arrives as nine JAR files of its own. Every teammate repeats the hunt, some end up with different versions, and the commands keep getting longer.

A build tool is a program that does this work for you. You describe the project once, in a file: its name, the Java version, the libraries it needs. Then one command:

  • compiles every source file, keeping the program and its tests apart;
  • downloads the libraries you named, plus the libraries those need, in versions that fit together;
  • runs the tests, and stops if one fails;
  • packs the compiled classes into a JAR;
  • and does all of it the same way on your laptop, on a teammate's computer and on the server that checks every change.
Key idea

A build tool reads a description of your project and turns the source code into tested, packaged code with one command, the same way on every computer.

Two tools you will meet
Most Java projects use Maven or Gradle. Both expect the same folders, download libraries from the same place and run the same JUnit tests, so most of what you learn about one carries over. This page goes deep on Maven first, because its build file spells everything out and its steps always run in the same order. Then it builds the same project with Gradle.

The standard project layout#

A build tool needs to know where your code is. Instead of telling it, you put every file where it expects to find one. Here is the bank project, laid out for Maven:

Project layout
read along
1bank/
2├── pom.xml
3└── src/
4 ├── main/
5 │ └── java/
6 │ └── com/
7 │ └── example/
8 │ └── bank/
9 │ ├── BankAccount.java
10 │ └── Main.java
11 └── test/
12 └── java/
13 └── com/
14 └── example/
15 └── bank/
16 └── BankAccountTest.java
  • pom.xml sits at the top and describes the project. The next section reads it line by line.
  • src/main/java holds the program's own code. Inside it the folders follow the package name, as in packages and imports: com.example.bank becomes com/example/bank.
  • src/test/java holds the tests, in the same package as the class they test. Because the package matches, a test can use the class's package-private members, while the test code stays out of the finished program.
  • src/main/resources and src/test/resources, when you need them, hold files that are not code, such as a settings file or test data. The build copies them next to the compiled classes.

When you build, Maven writes everything it produces into a new folder, target: compiled classes, test reports, the finished JAR. You never edit anything inside it, and you never commit it, because the next build makes it again from src. That is why the .gitignore in Git and GitHub lists target/. Gradle uses the same src folders and writes its output to build instead.

FolderWhat goes in itCommit it?
src/main/javaThe program's source code, in package foldersYes
src/main/resourcesFiles the program reads, such as settingsYes
src/test/javaTest classes, in the same packages as the classes they testYes
src/test/resourcesFiles that only the tests readYes
target/ (Maven), build/ (Gradle)Everything the build producesNo
The standard layout. Maven and Gradle both use it.

Agreeing on where things go, so that nobody has to configure it, is called convention over configuration. It pays off every time you open someone else's project: the code is in src/main/java and the tests are in src/test/java, whoever wrote it.

Quick check
BankAccountTest is in the package com.example.bank. Where does its file go?

The pom.xml: describing the project#

Maven reads one file, pom.xml. POM stands for Project Object Model, Maven's name for its description of a project. The file is written in XML, a text format where each value sits between an opening and a closing tag, such as <version>1.0-SNAPSHOT</version>, and tags can sit inside other tags. Here is the whole file for the bank project:

pom.xml
read along
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5 <modelVersion>4.0.0</modelVersion>
6
7 <groupId>com.example</groupId>
8 <artifactId>bank</artifactId>
9 <version>1.0-SNAPSHOT</version>
10
11 <properties>
12 <maven.compiler.release>25</maven.compiler.release>
13 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
14 </properties>
15
16 <dependencies>
17 <dependency>
18 <groupId>org.apache.commons</groupId>
19 <artifactId>commons-text</artifactId>
20 <version>1.15.0</version>
21 </dependency>
22 <dependency>
23 <groupId>org.junit.jupiter</groupId>
24 <artifactId>junit-jupiter</artifactId>
25 <version>6.1.3</version>
26 <scope>test</scope>
27 </dependency>
28 </dependencies>
29</project>
  • The first four lines say which kind of XML this is, so editors can check the file as you type. Every pom starts this way. Copy them and leave them alone.
  • modelVersion is the version of the POM format itself. It is 4.0.0 in every Maven 3 project.
  • groupId, artifactId and version name the project. They are its coordinates, explained below.
  • properties holds settings. maven.compiler.release set to 25 compiles the code for Java 25. project.build.sourceEncoding tells Maven to read the source files as UTF-8, so a name such as Zoë in your code survives on every computer.
  • dependencies lists the libraries the project needs, one dependency block each.

Coordinates: a project's address#

Every project, yours and every library on the internet, is named by three values called its coordinates:

  • groupId says who makes it. By convention it is a reversed domain name, like a package name: com.example here, org.apache.commons for Commons Text.
  • artifactId is the project's own name, bank. An artifact is a file that a build produces, usually a JAR.
  • version says which release. 1.0-SNAPSHOT means work in progress: a SNAPSHOT version is still changing, while a release version such as 1.15.0 never changes once it is published.

Written on one line with colons, Commons Text's coordinates are org.apache.commons:commons-text:1.15.0 and the bank project's are com.example:bank:1.0-SNAPSHOT. The JAR that Maven builds takes its name from the last two: bank-1.0-SNAPSHOT.jar.

Key idea

A groupId, an artifactId and a version make up a project's address. Give Maven a library's address, and it can find the library, download it and put it on the classpath.

You rarely type a pom from scratch
Your editor writes the first version for you. In IntelliJ IDEA, the New Project window asks which build system to use (Maven or Gradle) and fills in the group, the artifact and the version. Maven can also make a starter project from a template with mvn archetype:generate. Either way, you end up editing a file like the one above.

Dependencies and Maven Central#

A dependency is another project that your project needs, named by its coordinates. The bank project has two, and they differ in their scope: which part of the build can use them. Commons Text has no scope line, so it gets the default, compile, which means the program needs it to compile and to run. JUnit has <scope>test</scope>: it is there only while the tests compile and run, so code in src/main/java cannot use it.

Maven downloads dependencies from Maven Central, a free public store of Java libraries at https://repo.maven.apache.org/maven2. You search it at central.sonatype.com, where every library has a page with its versions and the exact lines to paste into a pom or a Gradle file. Downloads are saved in your local repository, the folder .m2/repository in your home folder. Every project on your computer shares it, so each version of a library is downloaded once.

Here is the first mvn compile in the bank project, on a computer that has never run Maven. Most of the 200 download lines are cut; these are the ones for the two libraries:

Terminal
read along
1$ mvn compile
2[INFO] Scanning for projects...
3[INFO]
4[INFO] --------------------------< com.example:bank >--------------------------
5[INFO] Building bank 1.0-SNAPSHOT
6[INFO] from pom.xml
7[INFO] --------------------------------[ jar ]---------------------------------
8[INFO] Loaded 24138 auto-discovered prefixes for remote repository central (prefixes-central.txt)
9...
10Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-text/1.15.0/commons-text-1.15.0.pom
11...
12Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-text/1.15.0/commons-text-1.15.0.pom (24 kB at 59 kB/s)
13...
14Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.pom
15...
16Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.pom (33 kB at 256 kB/s)
17...
18Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-text/1.15.0/commons-text-1.15.0.jar
19Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-text/1.15.0/commons-text-1.15.0.jar (265 kB at 652 kB/s)
20Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar
21Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.20.0/commons-lang3-3.20.0.jar (714 kB at 1.3 MB/s)
22...
23[INFO] --- compiler:3.16.0:compile (default-compile) @ bank ---
24...
25[INFO] Recompiling the module because of changed source code.
26[INFO] Compiling 2 source files with javac [debug release 25] to target/classes
27[INFO] ------------------------------------------------------------------------
28[INFO] BUILD SUCCESS
29[INFO] ------------------------------------------------------------------------
30[INFO] Total time: 10.179 s
31[INFO] Finished at: 2026-10-19T14:11:29+03:00
32[INFO] ------------------------------------------------------------------------

Look at the addresses. The coordinates turn into folders: the dots of org.apache.commons become org/apache/commons, then come the artifact and the version. It is the same mirror-the-folders rule that packages follow.

Maven fetched Commons Lang without being asked. It downloaded Commons Text's own pom first (the .pom file), found Commons Lang listed there as a dependency, and fetched that too. A library you get because another library needs it is a transitive dependency. Most of the cut lines are Maven's own plugins, the pieces of Maven that do the actual work, such as the compiler plugin. They come from Maven Central as well, which is why the first build is slow and the ones after it are quick.

Seeing the whole tree#

mvn dependency:tree prints every library the project gets, and which library brought it:

Terminal
read along
1$ mvn dependency:tree
2...
3[INFO] --- dependency:3.11.0:tree (default-cli) @ bank ---
4...
5[INFO] com.example:bank:jar:1.0-SNAPSHOT
6[INFO] +- org.apache.commons:commons-text:jar:1.15.0:compile
7[INFO] | \- org.apache.commons:commons-lang3:jar:3.20.0:compile
8[INFO] \- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
9[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
10[INFO] | +- org.opentest4j:opentest4j:jar:1.3.0:test
11[INFO] | +- org.junit.platform:junit-platform-commons:jar:6.1.3:test
12[INFO] | +- org.apiguardian:apiguardian-api:jar:1.1.2:test
13[INFO] | \- org.jspecify:jspecify:jar:1.0.0:test
14[INFO] +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test
15[INFO] \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
16[INFO] \- org.junit.platform:junit-platform-engine:jar:6.1.3:test
17[INFO] ------------------------------------------------------------------------
18[INFO] BUILD SUCCESS

Two dependencies in the pom became eleven JAR files. The +- and \- marks draw the tree: Commons Lang sits under Commons Text because Commons Text brought it in. The last word on each line is the scope. Everything under JUnit has the scope test too: a library that only the tests need brings its own libraries in only for the tests.

Quick check
The bank's pom lists Commons Text and JUnit. Which JARs does Maven put on the classpath when it compiles the code in src/main/java?

The lifecycle: compile, test, package#

You do not tell Maven how to build, step by step. You say how far you want to go, and Maven runs a fixed list of steps up to that point. The list is the lifecycle, and each step in it is a phase. These are the main phases, in order:

  1. 1validate checks that the pom is complete and correct.
  2. 2compile compiles src/main/java into target/classes.
  3. 3test compiles the tests and runs them.
  4. 4package packs the compiled classes into a JAR in target.
  5. 5verify runs any extra checks on the package. A plain project has none.
  6. 6install copies the JAR into your local repository, so other projects on your computer can depend on it.
  7. 7deploy uploads the JAR to a shared repository, for other people to use.
Key idea

Asking for a phase runs every phase before it, in order. mvn package compiles the code, runs the tests, and only then builds the JAR.

Each phase's work is done by a plugin. A line such as --- compiler:3.16.0:compile (default-compile) @ bank --- reads: the compiler plugin, version 3.16.0, is running its goal (one job) called compile, for the project bank. Here is the real output of mvn package on the bank project, after one earlier build:

Terminal
read along
1$ mvn package
2[INFO] Scanning for projects...
3[INFO]
4[INFO] --------------------------< com.example:bank >--------------------------
5[INFO] Building bank 1.0-SNAPSHOT
6[INFO] from pom.xml
7[INFO] --------------------------------[ jar ]---------------------------------
8[INFO] Loaded 24138 auto-discovered prefixes for remote repository central (prefixes-central.txt)
9[INFO]
10[INFO] --- resources:3.5.0:resources (default-resources) @ bank ---
11[INFO] skip non existing resourceDirectory /home/you/bank/src/main/resources
12[INFO]
13[INFO] --- compiler:3.16.0:compile (default-compile) @ bank ---
14[INFO] Loaded 74 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt)
15[INFO] Nothing to compile - all classes are up to date.
16[INFO]
17[INFO] --- resources:3.5.0:testResources (default-testResources) @ bank ---
18[INFO] skip non existing resourceDirectory /home/you/bank/src/test/resources
19[INFO]
20[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ bank ---
21[INFO] Nothing to compile - all classes are up to date.
22[INFO]
23[INFO] --- surefire:3.6.0:test (default-test) @ bank ---
24[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
25[INFO]
26[INFO] -------------------------------------------------------
27[INFO] T E S T S
28[INFO] -------------------------------------------------------
29[INFO] Running com.example.bank.BankAccountTest
30[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.056 s -- in com.example.bank.BankAccountTest
31[INFO]
32[INFO] Results:
33[INFO]
34[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
35[INFO]
36[INFO]
37[INFO] --- jar:3.5.1:jar (default-jar) @ bank ---
38[INFO] Building jar: /home/you/bank/target/bank-1.0-SNAPSHOT.jar
39[INFO] ------------------------------------------------------------------------
40[INFO] BUILD SUCCESS
41[INFO] ------------------------------------------------------------------------
42[INFO] Total time: 6.016 s
43[INFO] Finished at: 2026-10-19T14:12:31+03:00
44[INFO] ------------------------------------------------------------------------

Read it from top to bottom and you can watch the phases go by. The lines about auto-discovered prefixes are Maven's bookkeeping about which files each download site holds; skip them.

Line in the outputPhaseWhat it did
resources:resourcesprocess-resourcesCopies src/main/resources into target/classes. This project has none, so it skips.
compiler:compilecompileCompiles src/main/java into target/classes. Nothing had changed since the last build, so nothing needed compiling.
resources:testResourcesprocess-test-resourcesCopies src/test/resources. Skipped here too.
compiler:testCompiletest-compileCompiles src/test/java into target/test-classes.
surefire:testtestRuns every test class with Surefire, Maven's test runner, and writes reports into target/surefire-reports.
jar:jarpackagePacks target/classes into target/bank-1.0-SNAPSHOT.jar.
Six plugin goals ran for mvn package. A phase with no goal attached, such as validate, does nothing.

The full lifecycle has more phases than the seven famous ones. The ones in between, such as process-resources and test-compile, do the preparation, and you rarely name them yourself.

Quick check
One test fails during mvn package. What is in target afterwards?
Skipping the tests
Search the web for a failing Maven build and you will find advice to add -DskipTests. That tells Surefire not to run the tests, so the JAR gets built from code you know is broken. Fix the code or the test instead.

Starting clean, and installing#

mvn clean deletes the whole target folder, so the next build starts from nothing:

Terminal
read along
1$ mvn clean
2...
3[INFO] --- clean:3.5.0:clean (default-clean) @ bank ---
4[INFO] Deleting /home/you/bank/target
5[INFO] ------------------------------------------------------------------------
6[INFO] BUILD SUCCESS

clean belongs to a separate lifecycle, so it only runs when you ask for it. mvn clean package asks for both: first clean, then every phase up to package. That is the usual command when you want to be sure nothing old is left lying in target.

mvn install runs everything up to install, which copies the JAR and the pom into your local repository, under com/example/bank/1.0-SNAPSHOT. Another project on your computer can then list com.example:bank:1.0-SNAPSHOT as a dependency. deploy does the same for a shared repository on a server, which a team or a company sets up.

Running the program#

The JAR holds only your own classes: list it with jar tf target/bank-1.0-SNAPSHOT.jar and you find BankAccount.class and Main.class, and no Commons Text. So running it with plain java fails the way it did by hand:

Terminal
read along
1$ java -cp target/bank-1.0-SNAPSHOT.jar com.example.bank.Main
2Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/text/WordUtils
3 at com.example.bank.BankAccount.<init>(BankAccount.java:10)
4 at com.example.bank.Main.main(Main.java:5)
5Caused by: java.lang.ClassNotFoundException: org.apache.commons.text.WordUtils
6 at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
7 at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
8 ... 2 more

Let the build tool start it instead, because it knows the whole classpath. Your editor's Run button does exactly that. In a terminal, Maven's Exec plugin runs a main class with the project's libraries on the classpath:

Terminal
read along
1$ mvn exec:java -Dexec.mainClass=com.example.bank.Main
2...
3[INFO] --- exec:3.6.4:java (default-cli) @ bank ---
4[INFO] Loaded 50 auto-discovered prefixes for remote repository ow2-snapshot (prefixes-ow2-snapshot.txt)
5Ada Lovelace has 120
6[INFO] ------------------------------------------------------------------------
7[INFO] BUILD SUCCESS

exec:java is a goal, not a phase, so nothing runs before it and it compiles nothing. Run mvn compile first, or put both in one command: mvn compile exec:java -Dexec.mainClass=com.example.bank.Main.

A JAR that runs on its own
Making a JAR that starts with java -jar and a single file name, with its libraries included or listed, is the subject of packaging and shipping a Java app. Before that, logging adds a dependency of exactly this kind.

Reading a failed build#

A failed build prints a lot, and most of it looks the same every time. Read it in this order:

  1. 1Go to the bottom. BUILD FAILURE confirms that it failed, and the line that starts Failed to execute goal names what failed: the compiler, Surefire, or the download of a dependency.
  2. 2Find the first [ERROR] line above it that names a file, a test or a library. That line is the cause.
  3. 3Skip the lines about -e, -X and [Help 1]. They appear under every failure and only offer more detail.

A compile error#

Someone mistyped balance as balanse in deposit:

Terminal
read along
1$ mvn test
2...
3[INFO] --- compiler:3.16.0:compile (default-compile) @ bank ---
4[INFO] Loaded 74 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt)
5[INFO] Recompiling the module because of changed source code.
6[INFO] Compiling 2 source files with javac [debug release 25] to target/classes
7[INFO] -------------------------------------------------------------
8[ERROR] COMPILATION ERROR :
9[INFO] -------------------------------------------------------------
10[ERROR] /home/you/bank/src/main/java/com/example/bank/BankAccount.java:[18,19] cannot find symbol
11 symbol: variable balanse
12 location: class com.example.bank.BankAccount
13[INFO] 1 error
14[INFO] -------------------------------------------------------------
15[INFO] ------------------------------------------------------------------------
16[ERROR] BUILD FAILURE
17[INFO] ------------------------------------------------------------------------
18[INFO] Total time: 4.379 s
19[INFO] Finished at: 2026-10-19T14:14:00+03:00
20[INFO] ------------------------------------------------------------------------
21[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.16.0:compile (default-compile) on project bank: Compilation failure
22[ERROR] /home/you/bank/src/main/java/com/example/bank/BankAccount.java:[18,19] cannot find symbol
23[ERROR] symbol: variable balanse
24[ERROR] location: class com.example.bank.BankAccount
25[ERROR] -> [Help 1]
26[ERROR]
27[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
28[ERROR] Re-run Maven using the -X switch to enable full debug logging.
29[ERROR]
30[ERROR] For more information about the errors and possible solutions, please read the following articles:
31[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

The goal at the bottom is compiler:compile, so the mistake is in src/main/java; a mistake in a test fails at compiler:testCompile instead. The file and position appear twice: BankAccount.java:[18,19] means line 18, column 19. The message is javac's own, the cannot find symbol you met in reading errors and debugging. Maven runs javac for you, so a compile error reads the way it always did. Notice too that the tests never ran: a build that cannot compile stops at compile.

A failing test#

Next, a new rule: an account must refuse to pay out more than it holds. The test comes first, added to the end of BankAccountTest, and withdraw does not check anything yet:

BankAccountTest.java (the new test)
read along
1 @Test
2 void withdrawRefusesMoreThanTheBalance() {
3 BankAccount account = new BankAccount("Ada", 100);
4 assertThrows(IllegalArgumentException.class, () -> account.withdraw(500));
5 }
Terminal
read along
1$ mvn test
2...
3[INFO] --- surefire:3.6.0:test (default-test) @ bank ---
4[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
5[INFO]
6[INFO] -------------------------------------------------------
7[INFO] T E S T S
8[INFO] -------------------------------------------------------
9[INFO] Running com.example.bank.BankAccountTest
10[ERROR] Tests run: 5, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.104 s <<< FAILURE! -- in com.example.bank.BankAccountTest
11[ERROR] com.example.bank.BankAccountTest.withdrawRefusesMoreThanTheBalance -- Time elapsed: 0.055 s <<< FAILURE!
12org.opentest4j.AssertionFailedError: Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.
13 at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3234)
14 at com.example.bank.BankAccountTest.withdrawRefusesMoreThanTheBalance(BankAccountTest.java:39)
15
16[INFO]
17[INFO] Results:
18[INFO]
19[ERROR] Failures:
20[ERROR] BankAccountTest.withdrawRefusesMoreThanTheBalance:39 Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.
21[INFO]
22[ERROR] Tests run: 5, Failures: 1, Errors: 0, Skipped: 0
23[INFO]
24[INFO] ------------------------------------------------------------------------
25[ERROR] BUILD FAILURE
26[INFO] ------------------------------------------------------------------------
27[INFO] Total time: 4.573 s
28[INFO] Finished at: 2026-10-19T14:14:20+03:00
29[INFO] ------------------------------------------------------------------------
30[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.6.0:test (default-test) on project bank: There are test failures.
31[ERROR]
32[ERROR] See /home/you/bank/target/surefire-reports for the individual test results.
33...

The summary under Results:names the test and the line where it failed (39), followed by JUnit's message: the test expected an IllegalArgumentException and nothing was thrown. Tests run: 5, Failures: 1 is the count. A failure means an assertion did not hold; an error, counted separately, means a test crashed with an exception nobody expected. The details for every test class are saved in target/surefire-reports, as the last lines say.

ExerciseBankAccountTest.java
Make the build pass. In the project, BankAccount and its tests are two files; here they share one snippet so the Run button can run the tests (it cannot download Commons Text, so this copy has no owner name). Change withdraw so that it throws an IllegalArgumentException with the message Not enough money when the amount is larger than the balance. Leave the tests alone. The last test checks that taking out the whole balance still works.

A dependency that cannot be found#

Last, a typo in the pom: 1.51.0 instead of 1.15.0 as the Commons Text version.

Terminal
read along
1$ mvn compile
2...
3Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-text/1.51.0/commons-text-1.51.0.pom
4[WARNING] The POM for org.apache.commons:commons-text:jar:1.51.0 is missing, no dependency information available
5Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-text/1.51.0/commons-text-1.51.0.jar
6[INFO] ------------------------------------------------------------------------
7[ERROR] BUILD FAILURE
8[INFO] ------------------------------------------------------------------------
9[INFO] Total time: 4.199 s
10[INFO] Finished at: 2026-10-19T14:15:19+03:00
11[INFO] ------------------------------------------------------------------------
12[ERROR] Failed to execute goal on project bank: Could not resolve dependencies for project com.example:bank:jar:1.0-SNAPSHOT
13[ERROR] dependency: org.apache.commons:commons-text:jar:1.51.0 (compile)
14[ERROR] Could not find artifact org.apache.commons:commons-text:jar:1.51.0 in central (https://repo.maven.apache.org/maven2)
15...

No plugin ran at all. Before the first phase starts, Maven must have every dependency, and Maven Central has no Commons Text 1.51.0. The [WARNING] line was the first sign: the pom for that version is missing. When you see Could not find artifact, check the coordinates and the version against the library's page on Maven Central. If they are right, check your internet connection, because a project's first build needs one.

The bottom of the output saysWhat went wrongWhere to look
maven-compiler-plugin ... :compile ... Compilation failureA compile error in the programThe file and [line,column] in the COMPILATION ERROR block, under src/main/java
maven-compiler-plugin ... :testCompile ... Compilation failureA compile error in a testThe same block, under src/test/java
maven-surefire-plugin ... There are test failures.A test failedThe Results: summary, then target/surefire-reports
Could not resolve dependenciesA library could not be downloadedThe coordinates in pom.xml, then your connection
The four failures you will see most often.

The same project in Gradle#

Gradle builds the same project from the same folders. Its build files are short programs instead of XML, written in Kotlin (build.gradle.kts) or in Groovy (build.gradle). Both languages describe the same build. Kotlin has been the default for new projects since Gradle 8.2, and this page uses it; you do not need to know Kotlin to read one. Two files take the place of pom.xml:

settings.gradle.kts
read along
1rootProject.name = "bank"
build.gradle.kts
read along
1plugins {
2 application
3}
4
5group = "com.example"
6version = "1.0-SNAPSHOT"
7
8repositories {
9 mavenCentral()
10}
11
12dependencies {
13 implementation("org.apache.commons:commons-text:1.15.0")
14 testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
15 testRuntimeOnly("org.junit.platform:junit-platform-launcher")
16}
17
18java {
19 toolchain {
20 languageVersion = JavaLanguageVersion.of(25)
21 }
22}
23
24application {
25 mainClass = "com.example.bank.Main"
26}
27
28tasks.test {
29 useJUnitPlatform()
30}
  • plugins { application }: Gradle knows nothing about Java until a plugin teaches it. The application plugin brings everything for compiling, testing and making a JAR, plus a run task for a program with a main method.
  • group and version are two of the coordinates. The third, the name, is rootProject.name in settings.gradle.kts.
  • repositories { mavenCentral() } says where to download from. Gradle uses Maven Central too, but only when the build file says so.
  • implementation is Maven's compile scope and testImplementation is its test scope. Each dependency is one string of coordinates, group:name:version.
  • testRuntimeOnly adds the JUnit Platform launcher, the part of JUnit that starts the tests. Since Gradle 9 you must list it yourself. It needs no version, because JUnit's own files tell Gradle which launcher goes with JUnit 6.1.3.
  • The toolchain block compiles and runs everything with Java 25. If Gradle finds no JDK 25 on your computer, it stops and says so.
  • mainClass tells the run task which class to start, and useJUnitPlatform() makes the test task run JUnit tests.

Tasks instead of phases#

Gradle has no fixed lifecycle. Its work is split into tasks, and each task names the tasks it needs done first. Ask for test and Gradle works out the rest. In a terminal, a passing build says very little:

Terminal
read along
1$ gradle clean test
2
3BUILD SUCCESSFUL in 18s
44 actionable tasks: 4 executed
5Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

The last line is a tip about a setting that makes later builds faster, and you can ignore it. To see the tasks, add --console=plain. This is the very first gradle test in the project, which also downloaded the libraries:

Terminal
read along
1$ gradle test --console=plain
2> Task :compileJava
3> Task :processResources NO-SOURCE
4> Task :classes
5> Task :compileTestJava
6> Task :processTestResources NO-SOURCE
7> Task :testClasses
8> Task :test
9
10BUILD SUCCESSFUL in 1m 18s
113 actionable tasks: 3 executed
12Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

The tasks line up with Maven's phases: compileJava is compile, compileTestJava is test-compile, and test is test. NO-SOURCE means the task had nothing to work on, because there is no resources folder. Now run the same command again without changing a thing:

Terminal
read along
1$ gradle test --console=plain
2> Task :compileJava UP-TO-DATE
3> Task :processResources NO-SOURCE
4> Task :classes UP-TO-DATE
5> Task :compileTestJava UP-TO-DATE
6> Task :processTestResources NO-SOURCE
7> Task :testClasses UP-TO-DATE
8> Task :test UP-TO-DATE
9
10BUILD SUCCESSFUL in 8s
113 actionable tasks: 3 up-to-date
12Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

Every task is UP-TO-DATE. Gradle remembers what each task read and wrote, and skips a task when none of it has changed, the tests included. Change one file and only the tasks that depend on it run again. Maven skips compiling unchanged code too (the Nothing to compile lines earlier), but it runs every test on every build.

Running, and reading Gradle's failures#

The run task starts Main with the whole classpath, Commons Text included:

Terminal
read along
1$ gradle run
2
3> Task :run
4Ada Lovelace has 120
5
6BUILD SUCCESSFUL in 9s
72 actionable tasks: 1 executed, 1 up-to-date
8Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

Here is the failing withdrawtest from before, run by Gradle. It names the test and the line, and puts JUnit's full message in a report you open in a browser:

Terminal
read along
1$ gradle test
2
3> Task :test
4
5BankAccountTest > withdrawRefusesMoreThanTheBalance() FAILED
6 org.opentest4j.AssertionFailedError at BankAccountTest.java:39
7
85 tests completed, 1 failed
9
10> Task :test FAILED
11
12FAILURE: Build failed with an exception.
13
14* What went wrong:
15Execution failed for task ':test'.
16> There were failing tests. See the report at: file:///home/you/bank/build/reports/tests/test/index.html
17
18* Try:
19> Run with --scan to get full insights from a Build Scan (powered by Develocity).
20
21BUILD FAILED in 19s
223 actionable tasks: 1 executed, 2 up-to-date

Gradle's failures follow one pattern. Under * What went wrong: each > line explains the line above it, so the last, most indented one is the real cause. This is what you get if you leave out the junit-platform-launcher line:

Terminal
read along
1$ gradle test
2
3> Task :test FAILED
4
5Gradle Test Executor 7 FAILED
6 org.gradle.api.internal.tasks.testing.TestSuiteExecutionException at SuiteTestDefinitionProcessor.java:45
7 Caused by: org.gradle.api.internal.tasks.testing.RequiresTestFrameworkTestDefinitionProcessor$TestFrameworkNotAvailableException at JUnitPlatformTestDefinitionProcessor.java:101
8
9FAILURE: Build failed with an exception.
10
11* What went wrong:
12Execution failed for task ':test' (registered by plugin 'org.gradle.jvm-test-suite').
13> Test process encountered an unexpected problem.
14 > Could not start Gradle Test Executor 7.
15 > Failed to load JUnit Platform. Please ensure that all JUnit Platform dependencies are available on the test's runtime classpath, including the JUnit Platform launcher.
16
17* Try:
18> Check common problems https://docs.gradle.org/9.8.1/userguide/java_testing.html#sec:java_testing_troubleshooting.
19> Run with --stacktrace option to get the stack trace.
20> Run with --info or --debug option to get more log output.
21> Run with --scan to get full insights from a Build Scan (powered by Develocity).
22> Get more help at https://help.gradle.org.
23
24BUILD FAILED in 20s
253 actionable tasks: 2 executed, 1 up-to-date

Read down the > lines: the test task failed because its test process hit a problem, because the process could not start, because JUnit Platform could not be loaded, and the last line names the missing piece, the launcher. Put the testRuntimeOnly line back and the tests run.

The wrapper: mvnw and gradlew#

One problem is left. Your pom works with the Maven on your computer, but a teammate may have an older Maven or none at all, and the build server may have a third version. The wrappersolves this. It is a small script, kept in the project, that runs one exact version of the build tool and downloads that version the first time it is needed. Maven's wrapper plugin adds one:

Terminal
read along
1$ mvn wrapper:wrapper
2...
3[INFO] --- wrapper:3.3.4:wrapper (default-cli) @ bank ---
4...
5[INFO] Unpacked only-script type wrapper distribution org.apache.maven.wrapper:maven-wrapper-distribution:zip:only-script:3.3.4
6[INFO] Configuring .mvn/wrapper/maven-wrapper.properties to use Maven 3.10.0 and download from https://repo.maven.apache.org/maven2
7[INFO] ------------------------------------------------------------------------
8[INFO] BUILD SUCCESS

It adds three files: the scripts mvnw (for macOS and Linux) and mvnw.cmd (for Windows), and a settings file that names the version:

.mvn/wrapper/maven-wrapper.properties
read along
1wrapperVersion=3.3.4
2distributionType=only-script
3distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.10.0/apache-maven-3.10.0-bin.zip

From now on you type ./mvnw where you typed mvn: ./mvnw test, ./mvnw package. On Windows the command is mvnw.cmd test (in PowerShell, .\mvnw.cmd test). The first run quietly downloads Maven 3.10.0 into .m2/wrapper/dists in your home folder, and after that the output is exactly what mvn prints. To move the project to a newer Maven, change the version in distributionUrl and commit the change; everyone gets it with their next pull.

gradle wrapper does the same for Gradle, and gradle init adds the wrapper to every new project. It writes four files: gradlew, gradlew.bat, and in gradle/wrapper a small gradle-wrapper.jar that does the downloading and a gradle-wrapper.properties that names the version. When Grace from Git and GitHub clones the project onto a computer with a JDK and nothing else, her first build looks like this:

Terminal
read along
1$ ./gradlew test
2Fetching distribution.
3Downloading https://services.gradle.org/distributions/gradle-9.8.1-bin.zip
4..............10%..............20%...............30%..............40%...............50%..............60%...............70%..............80%...............90%..............100%
5
6Welcome to Gradle 9.8.1!
7
8Here are the highlights of this release:
9 - Java 27 support
10 - Maven mirror settings reuse
11 - Linked problem locations in build output
12
13For more details see https://docs.gradle.org/9.8.1/release-notes.html
14
15Starting a Gradle Daemon (subsequent builds will be faster)
16
17BUILD SUCCESSFUL in 4m 27s
183 actionable tasks: 3 executed
19Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

The download happens once for each Gradle version, and the welcome message appears only that first time, so the next build takes seconds. Starting a Gradle Daemon appears whenever none is running yet. The daemon is a Gradle process that stays in the background between builds, so the next build starts faster.

This only works if the wrapper files are in the repository, so commit them. Gradle's documentation says to commit all four, the JAR included.

File or folderCommit it?Why
pom.xml, build.gradle.kts, settings.gradle.ktsYesThey describe the build.
mvnw, mvnw.cmd, .mvn/wrapper/YesAnyone with a JDK can build without installing Maven.
gradlew, gradlew.bat, gradle/wrapper/YesAnyone with a JDK can build without installing Gradle.
target/, build/, .gradle/NoThe build makes them again from the source. .gradle/ is Gradle's working cache for the project, so add it to your .gitignore next to target/ and build/.
What goes into Git in a Maven or Gradle project.
Key idea

Commit the wrapper, and anyone with a JDK can build the project with ./mvnw or ./gradlew, using exactly the tool version the project names.

Quick check
Grace clones a project that has mvnw and maven-wrapper.properties, but she has never installed Maven. What happens when she runs ./mvnw test?

Maven or Gradle?#

Both tools build Java projects well, and most of this page applies to both: the src layout, coordinates, Maven Central, scopes, the wrapper. They differ in how you describe the build and in how much work they skip.

Maven
Gradle
The build file is pom.xml, in XML.
The build file is build.gradle.kts in Kotlin, or build.gradle in Groovy.
A fixed lifecycle: ask for a phase and every earlier phase runs.
A graph of tasks: ask for a task and the tasks it needs run.
Output goes to target.
Output goes to build.
Compiles only changed code, but runs every test on every build.
Skips every task whose inputs have not changed, the tests included.
Starts from scratch for every command.
Keeps a daemon running between builds, so they start faster.
Very regular: two Maven projects look and behave alike.
Flexible: a build file can hold real code, which helps big builds and can make small ones harder to read.
The wrapper is mvnw.
The wrapper is gradlew.

You will meet both. Android apps are built with Gradle, and Spring Initializr at start.spring.io, where many server projects begin, offers either one. When you join a project, use whatever it already uses. When you start your own, either is a fine choice: Maven's fixed steps make it the easier one to learn first, and Gradle's skipping saves more time as a build grows.

Installing the tools
A project with a wrapper needs only a JDK. To start new projects, install the tool once: download Maven from maven.apache.org or Gradle from gradle.org, unpack it and add its bin folder to your PATH, or use a package manager such as Homebrew (brew install maven, brew install gradle) or SDKMAN (sdk install maven, sdk install gradle). Check it with mvn -v or gradle -v. As this page is written, Maven 3.10.0 is the current release and Maven 4 is in preview.

Try it

Answer each one in your head or on paper first, then open the answer.

1
Name four things a build tool does for you that javac and java do not.
2
Read the coordinates org.junit.jupiter:junit-jupiter:6.1.3. Which part is the groupId, which the artifactId and which the version, and where does Maven save the JAR on your computer?
3
Which phases run for mvn test, and is there a JAR in target afterwards?
4
A build ends with Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.16.0:testCompile (default-testCompile) on project bank: Compilation failure. Which folder holds the mistake?
5
A Gradle build stops with Failed to load JUnit Platform, and the message mentions the JUnit Platform launcher. What is missing from build.gradle.kts?
6
Which parts of the Maven bank project go into Git, and which stay out?

Practice

Work through these on your own. Each one builds on this page.

1
Recreate the bank project on your computer with the files from this page (write the four tests yourself). Run mvn package, list the JAR with jar tf target/bank-1.0-SNAPSHOT.jar, and run mvn dependency:tree. Then find commons-text-1.15.0.jar inside .m2/repository in your home folder.
2
Break the build three ways on purpose, one at a time: a typo in BankAccount.java, a wrong number in an assertEquals, and a wrong version in the pom. For each, write down the single line of output that told you what was wrong, and which plugin or step failed.
3
Add the Gradle files from this page next to the pom. Run gradle test --console=plain twice, then change one line in BankAccount.java and run it a third time. Which tasks ran the third time, and which were UP-TO-DATE? Compare with running mvn test twice.
4
Add both wrappers, commit the project with a .gitignore that leaves out target/, build/ and .gradle/, clone it into a second folder as in Git and GitHub, and build the clone with ./mvnw test and ./gradlew test.

Further reading#

Short guides on this topic

Quick answers to what people search for about it, with code to run.

Spotted a mistake, or a part that did not make sense? Report it by email