Build Tools in Java: Maven and Gradle

Naveen AnamaneniUpdated · 10 min read

Baking one cake at home is easy: you remember the recipe, buy the flour and switch on the oven. A bakery that makes five hundred cakes a day can't work from memory. It has a written recipe card, a supplier who delivers the same flour every morning, and a fixed order of steps that every baker follows. A build tool is that bakery system for Java. With one file in the project, it knows which libraries to download, how to compile the code, when to run the tests and how to package the result, and every developer on the team gets the same result with one command. In this lesson, you will learn the standard project layout, build a small project with Maven and then with Gradle, and see how the two compare.

What Is a Build Tool?

A build tool in Java is a program that is used to automate building a project: downloading the libraries it depends on, compiling the source code, running the tests, and packaging the classes into a JAR. You describe the project once in a build file, and the tool does the rest.

Without a build toolWith Maven or Gradle
Download every library JAR by hand, and the JARs those needWrite one line per library; the tool downloads it and everything it needs
Long javac -cp ... and java -cp ... commandsmvn package or ./gradlew build
Tests run only if someone remembersTests run on every build, and a failing test stops it
Each developer's setup is a little differentThe build file is committed, so everyone builds the same way

Maven (2004) describes a project in an XML file called pom.xml. Gradle (2012) uses a script, build.gradle.kts in Kotlin or build.gradle in Groovy. Both are used widely: Maven is common in enterprise projects, and Gradle is the standard for Android and popular for large builds.

The Standard Project Layout

Both tools expect the same folder structure, so a Java developer can open any project and find things immediately.

One layout, and a lifecycle of phases
FolderWhat goes in it
src/main/javaApplication source code, in package folders
src/main/resourcesFiles copied into the JAR as they are, such as configuration
src/test/javaTest classes, never packaged into the JAR
target/ (Maven) or build/ (Gradle)Everything the tool generates; safe to delete, never committed

Maven

Let's build a small project, fee-app, that calculates school fees. It has three classes in src/main/java/school/fees, a test class in src/test/java/school/fees, and it uses Google's Gson library to turn an object into JSON.

App.java
package school.fees;
 
import com.google.gson.Gson;
 
public class App {
    public static void main(String[] args) {
        FeeCalculator calc = new FeeCalculator(500);
        Student s = new Student("Navika", 15000, 3);
        System.out.println(new Gson().toJson(s));
        System.out.println("Total: "
                + calc.totalFee(s.baseFee(), s.lateDays()));
    }
}

The pom.xml File

The POM (Project Object Model) describes the project for Maven:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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.school</groupId>
  <artifactId>fee-app</artifactId>
  <version>1.0.0</version>
 
  <properties>
    <maven.compiler.release>25</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
 
  <dependencies>
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.15.0</version>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
 
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.6.1</version>
      </plugin>
    </plugins>
  </build>
</project>

Explanation:

  1. groupId, artifactId and version are the project's coordinates, its unique name. groupId is usually a reversed domain, like a package name.
  2. maven.compiler.release compiles for Java 25, the same as javac --release 25.
  3. Each <dependency> names a library by its coordinates. Maven downloads it from Maven Central, the public library repository, and stores it in a local cache (~/.m2/repository), so it is downloaded only once.
  4. <scope>test</scope> makes JUnit available to the tests only. It is not part of the application.
  5. The Surefire plugin runs the tests. Pinning plugin versions keeps the build the same on every machine.

Building with Maven

Maven builds in a fixed order of phases called the lifecycle: validate, compile, test, package, verify, install and deploy. When you ask for a phase, Maven runs every phase before it first.

Terminal
mvn clean package
Terminal output
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.example.school:fee-app >---------------------
[INFO] Building fee-app 1.0.0
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- clean:3.5.0:clean (default-clean) @ fee-app ---
[INFO] Deleting ~/fee-app/target
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ fee-app ---
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ fee-app ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 3 source files with javac [debug release 25] to target/classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ fee-app ---
[INFO] skip non existing resourceDirectory ~/fee-app/src/test/resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ fee-app ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 1 source file with javac [debug release 25] to target/test-classes
[INFO]
[INFO] --- surefire:3.6.1:test (default-test) @ fee-app ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running school.fees.FeeCalculatorTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.017 s -- in school.fees.FeeCalculatorTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jar:3.5.1:jar (default-jar) @ fee-app ---
[INFO] Building jar: ~/fee-app/target/fee-app-1.0.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.545 s
[INFO] Finished at: 2026-11-14T09:31:14-06:00
[INFO] ------------------------------------------------------------------------

Explanation: clean deleted the old target folder. Asking for package then ran the phases before it in order: copy resources, compile the 3 main classes, compile the test class, run the 3 tests, and finally build target/fee-app-1.0.0.jar. Each --- name:version:goal --- line is a plugin goal attached to a phase; Maven itself only coordinates plugins. If a test had failed, the build would have stopped there with BUILD FAILURE and no JAR.

To run the program with its dependencies on the class path, the Exec plugin is the quickest way:

Terminal
mvn -q compile exec:java -Dexec.mainClass=school.fees.App
Terminal output
{"name":"Navika","baseFee":15000,"lateDays":3}
Total: 16500
CommandDescription
mvn compileIt is used to compile the main code into target/classes.
mvn testIt compiles everything and runs the tests.
mvn packageIt compiles, tests and builds the JAR in target/.
mvn installIt also copies the JAR into the local cache, so your other projects can use it.
mvn cleanIt deletes target/. Often combined: mvn clean package.
mvn dependency:treeIt shows every library the project uses and why.
mvn -q, mvn -oThey run quietly, or offline using only the local cache.

Gradle

Here is the same project built with Gradle. The src folder is identical; only the build files change.

settings.gradle.kts
rootProject.name = "fee-app"
build.gradle.kts
plugins {
    application
}
 
group = "com.example.school"
version = "1.0.0"
 
repositories {
    mavenCentral()
}
 
dependencies {
    implementation("com.google.code.gson:gson:2.15.0")
    testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
 
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}
 
application {
    mainClass = "school.fees.App"
}
 
tasks.test {
    useJUnitPlatform()
}

Explanation:

  1. The application plugin adds Java compilation plus run and distribution tasks.
  2. repositories { mavenCentral() }: Gradle uses the same Maven Central libraries and the same coordinates, written as one string "group:artifact:version".
  3. implementation is a library the code needs; testImplementation is for tests only, like Maven's test scope.
  4. The toolchain asks for JDK 25 to compile and run, whatever JDK started Gradle.
  5. useJUnitPlatform() tells the test task to run JUnit 5 and later tests.

Building with Gradle

Gradle works with tasks that depend on each other. build depends on assemble (compile and package) and check (run the tests):

Terminal
./gradlew clean build --console=plain
Terminal output
> Task :clean
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :startScripts
> Task :distTar
> Task :distZip
> Task :assemble
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :check
> Task :build
 
BUILD SUCCESSFUL in 602ms
8 actionable tasks: 8 executed
Terminal
./gradlew run -q
Terminal output
{"name":"Navika","baseFee":15000,"lateDays":3}
Total: 16500

Explanation: Gradle compiled, packaged build/libs/fee-app-1.0.0.jar, made ready-to-ship .zip and .tar distributions with start scripts, and ran the tests, all in well under a second. Gradle is incremental: run build again without changing anything and every task is reported UP-TO-DATE and skipped.

The Wrapper

./gradlew is the Gradle wrapper: a small script committed with the project that downloads the exact Gradle version the project needs, the first time it runs. Nobody has to install Gradle, and everyone uses the same version. Maven has the same idea, ./mvnw, created with mvn wrapper:wrapper.

Gradle commandDescription
./gradlew buildIt is used to compile, test and package the project.
./gradlew testIt runs the tests.
./gradlew runIt runs the main class (with the application plugin).
./gradlew cleanIt deletes the build/ folder.
./gradlew dependenciesIt shows the dependency tree.
./gradlew tasksIt lists the available tasks.

Maven vs Gradle

PointMavenGradle
Build filepom.xml (XML, declarative)build.gradle.kts (Kotlin) or build.gradle (Groovy)
ModelFixed lifecycle of phasesGraph of tasks
FlexibilityStrict conventions; custom logic needs a pluginAny custom logic can be written in the script
SpeedGoodFaster on large builds: incremental tasks and a build cache
Learning curveEasier to read and predictMore to learn, more power
Common inEnterprise and Spring projectsAndroid, large multi-module builds

Both download from the same repositories and share the same project layout, so moving a project from one to the other is mostly rewriting the build file.

Common Mistakes

Committing target/ or build/. They are generated output. Add them to .gitignore (see "Git Basics for Java Projects").

Putting tests in src/main/java. They would be compiled into the JAR, and test libraries with test scope would not be found. Tests go in src/test/java.

Skipping tests to get a green build. mvn package -DskipTests hides problems; fix the failing test instead.

Not pinning versions. Without plugin and library versions, builds can change on their own when a new version is released.

Installing Gradle globally and ignoring the wrapper. A different Gradle version can fail on the project's script. Always use ./gradlew.

Practice

  1. Easy: Create the fee-app project with the layout above and the pom.xml, run mvn package, and open the JAR with jar tf target/fee-app-1.0.0.jar. Check that no test class is inside.
  2. Harder: Add the Gradle files to the same folder and run ./gradlew build twice. Compare the task list of the second run with the first, then change one line in App.java and see which tasks run again.

Quick check: Maven and Gradle

4 questions
1Where do test classes go in a Maven or Gradle project?
2What does mvn package do first?
3What is the Gradle wrapper for?
4Which Gradle configuration matches Maven's test scope?

Key Takeaways

Was this tutorial helpful?

Lesson 527 of 578