Java · Lesson 19 of 20
Build Tools: Maven and Gradle
Build Java projects with Maven and Gradle: project layout, pom.xml, build.gradle.kts, dependencies and scopes, lifecycle commands and wrappers.
- Intermediate
- 17 min read
- 4 objectives
Before this lessonLesson 18: Threads, Executors and CompletableFuture
What you will learn
- Explain what a build tool does and why every real project uses one
- Read and write a Maven pom.xml and run the Maven lifecycle
- Set up the same project with Gradle's Kotlin DSL
- Manage dependency versions, scopes and wrappers
Your Progress
0 of 20 lessons 0%
- Lessons0 / 20
- Completed0
- Est. time left~ 5 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
So far you have run programs with java Main.java. That is perfect for learning, but a real application has dozens of source files, depends on libraries like Jackson or Spring, has tests to run, and must be packaged into a JAR that a server can start. Doing all that by hand with javac and a folder of downloaded JARs is fragile and slow.
A build tool automates it: it downloads dependencies (and their dependencies) from a repository such as Maven Central, compiles your code, runs tests, and packages the result, the same way on every machine and in CI. The Java world has two main tools: Maven and Gradle. You will meet both, so this lesson covers each.
The standard project layout
Both tools share the same conventional folder structure. If you follow it, you need almost no configuration, because the tool already knows where everything lives.
orders-service/
pom.xml (Maven) or build.gradle.kts + settings.gradle.kts (Gradle)
src/
main/
java/com/stackcone/orders/App.java
resources/application.properties
test/
java/com/stackcone/orders/AppTest.java
target/ (Maven output) or build/ (Gradle output)Production code goes in src/main/java, tests in src/test/java, and non-code files (config, templates) in resources. The output folder is generated, so add it to .gitignore.
Coordinates: how libraries are identified
Every library, including your own project, is identified by three coordinates: a groupId (usually a reversed domain, like com.fasterxml.jackson.core), an artifactId (the library name, jackson-databind) and a version. Written together they look like com.fasterxml.jackson.core:jackson-databind:2.18.2. You look these up on search.maven.org or in a library's README.
Maven: pom.xml
Maven describes a project in an XML file called pom.xml (Project Object Model). It is verbose but very predictable: you declare what the project is, and Maven's fixed lifecycle decides how to build it.
<?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.stackcone</groupId>
<artifactId>orders-service</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>
</project>maven.compiler.release sets the Java version. The 1.0.0-SNAPSHOT suffix marks a version still in development. Jackson is a normal dependency; JUnit has test scope, so it is available to tests but not shipped with the app. Plugins add or configure build steps; Surefire is the plugin that runs unit tests.
The Maven lifecycle
Maven builds happen in ordered phases. Running a phase runs every phase before it, so mvn package also validates, compiles and tests.
mvn compile # compile src/main/java into target/classes
mvn test # compile and run unit tests
mvn package # ... and build target/orders-service-1.0.0-SNAPSHOT.jar
mvn verify # ... and run integration checks
mvn install # ... and copy the JAR into your local ~/.m2 repository
mvn clean package # delete target/ first, then build from scratch
mvn dependency:tree # show every dependency and where it came from
mvn package -DskipTests # skip tests (use sparingly!)Downloaded libraries are cached in ~/.m2/repository, so the first build is slow and later ones are fast.
Gradle: build.gradle.kts
Gradle configures builds with code instead of XML. The Kotlin DSL (build.gradle.kts) is now the default for new projects and gives editor autocompletion; older projects use a Groovy build.gradle. Gradle is more flexible and usually faster thanks to incremental builds and a build cache, which is why Android and many large projects use it. Here is the same project:
// settings.gradle.kts
rootProject.name = "orders-service"
// build.gradle.kts
plugins {
application
}
group = "com.stackcone"
version = "1.0.0-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.18.2")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
application {
mainClass = "com.stackcone.orders.App"
}
tasks.test {
useJUnitPlatform()
}Gradle calls scopes configurations: implementation (compile and runtime), testImplementation (tests only), compileOnly and runtimeOnly. The toolchain block even downloads the right JDK if it is missing. Builds are made of tasks:
./gradlew build # compile, test and assemble build/libs/orders-service-1.0.0-SNAPSHOT.jar
./gradlew test # run tests only
./gradlew run # run the application (from the application plugin)
./gradlew clean build # rebuild from scratch
./gradlew dependencies # print the dependency tree
./gradlew tasks # list available tasksWrappers: pin the build tool version
Notice ./gradlew rather than gradle. The wrapper is a small script committed to the repository that downloads the exact build tool version the project expects. Anyone who clones the repo can build without installing Gradle, and everyone uses the same version. Maven has the same idea: ./mvnw. Generate them with gradle wrapper or mvn wrapper:wrapper, then commit the scripts and the small gradle/wrapper or .mvn/wrapper folder.
Maven or Gradle?
- Maven: convention over configuration, very predictable, huge ecosystem, easy to read; common in enterprise and Spring projects.
- Gradle: flexible and fast (incremental builds, build cache), required for Android, better for large multi-module builds with custom logic.
- Both use Maven Central and the same coordinates, so any library works with either. Spring Initializr (start.spring.io) generates both.
- Pick whatever your team already uses; for a new small project, either is fine.
Recap
- Build tools download dependencies, compile, test and package the same way on every machine.
- Both Maven and Gradle use the
src/main/javaandsrc/test/javalayout andgroup:artifact:versioncoordinates. - Maven uses
pom.xmland lifecycle phases (mvn clean package); scopes liketestcontrol where a dependency is used. - Gradle uses
build.gradle.kts, tasks (./gradlew build) and configurations likeimplementationandtestImplementation. - Commit the wrapper (
mvnworgradlew) and inspect dependency trees when versions clash.
// Write your solution here
Finished reading? Mark this lesson complete to track your progress.
