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 tasks

Wrappers: 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/java and src/test/java layout and group:artifact:version coordinates.
  • Maven uses pom.xml and lifecycle phases (mvn clean package); scopes like test control where a dependency is used.
  • Gradle uses build.gradle.kts, tasks (./gradlew build) and configurations like implementation and testImplementation.
  • Commit the wrapper (mvnw or gradlew) and inspect dependency trees when versions clash.
// Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 20Testing with JUnitWrite unit tests, assertions and run them with Maven or Gradle.