Skip to main content

    Lesson 38 • Advanced

    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.

    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

    • You'll be able to write a pom.xml with GAV coordinates and dependencies
    • You'll be able to run the Maven lifecycle: validate, compile, test, package, install
    • You'll be able to write a build.gradle with dependencies and tasks
    • You'll be able to add dependencies and pin exact versions safely
    • You'll be able to read a transitive dependency tree and fix version conflicts
    • You'll be able to choose Maven or Gradle for a given project

    🧱 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.

    PhaseCommandWhat it does
    validatemvn validateCheck the project is correct
    compilemvn compileCompile source to target/classes
    testmvn testRun the unit tests
    packagemvn packageBundle into a JAR/WAR
    installmvn installCopy 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.

    Create a Maven project and run 'mvn package'
    # Generate a new project with the standard directory layout.
    mvn archetype:generate \
      -DgroupId=com.mycompany \
      -DartifactId=my-app \
      -DarchetypeArtifactId=maven-archetype-quickstart \
      -DinteractiveMode=false
    
    cd my-app
    
    # The layout Maven creates AND expects (convention over configuration):
    #   src/main/java/        your application source code
    #   src/main/resources/   config files (application.properties)
    #   src/test/java/        your test code
    #   target/               build output (compiled classes + the JAR)
    #   pom.xml               the Project Object Model — deps + build config
    
    # Run the build. 'package' runs every phase BEFORE it, in order:
    #   validate -> compile -> test -> package
    mvn package
    Output
    [INFO] --- compiler:3.13.0:compile (default-compile) ---
    [INFO] Compiling 1 source file to /my-app/target/classes
    [INFO] --- surefire:3.2.5:test (default-test) ---
    [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
    [INFO] --- jar:3.4.1:jar (default-jar) ---
    [INFO] Building jar: /my-app/target/my-app-1.0.0.jar
    [INFO] BUILD SUCCESS
    [INFO] Total time:  4.218 s
    These are terminal commands plus the mvn package output. Run them in your own shell after installing a JDK and Maven; one command runs the whole compile -> test -> package chain.

    2️⃣ The pom.xml — Coordinates, Scopes, and Plugins

    The pom.xml (Project Object Model) is Maven's recipe. Three things make it work:

    • GAV coordinatesgroupId + 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.
    pom.xml — GAV coordinates, dependency scopes, a plugin
    <project xmlns="http://maven.apache.org/POM/4.0.0">
      <modelVersion>4.0.0</modelVersion>
    
      <!-- GAV coordinates: the three values that uniquely name your artifact. -->
      <groupId>com.mycompany</groupId>      <!-- who: reverse-domain namespace -->
      <artifactId>my-app</artifactId>       <!-- what: the project name -->
      <version>1.0.0</version>              <!-- which release -->
    
      <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      </properties>
    
      <dependencies>
        <!-- compile scope (the default): on the classpath everywhere. -->
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.17.0</version>         <!-- always PIN an exact version -->
        </dependency>
    
        <!-- test scope: only on the test classpath, not shipped in the JAR. -->
        <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter</artifactId>
          <version>5.10.2</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    
      <build>
        <plugins>
          <!-- Plugins add steps to the lifecycle. Shade builds a "fat JAR". -->
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
          </plugin>
        </plugins>
      </build>
    </project>
    This is a project's pom.xml, not runnable code. Save it at the project root and build with mvn package. Note the test scope and the pinned versions.

    🎯 Your Turn #1 — Finish the pom.xml

    Fill in the artifactId, version, and the dependency scope. The expected result is in the comments.

    Complete the Maven coordinates and scope
    <project xmlns="http://maven.apache.org/POM/4.0.0">
      <modelVersion>4.0.0</modelVersion>
    
      <!-- 🎯 YOUR TURN — fill in the blanks marked with ___ -->
    
      <!-- 1) Give this artifact its GAV coordinates. -->
      <groupId>com.example</groupId>
      <artifactId>___</artifactId>          <!-- 👉 name it "todo-app" -->
      <version>___</version>                <!-- 👉 use "1.0.0" -->
    
      <dependencies>
        <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter</artifactId>
          <version>5.10.2</version>
          <scope>___</scope>                <!-- 👉 tests only: use "test" -->
        </dependency>
      </dependencies>
    </project>
    
    <!-- ✅ Expected: 'mvn validate' prints BUILD SUCCESS with -->
    <!--    artifactId = todo-app, version = 1.0.0, JUnit on the TEST classpath. -->
    Replace each ___, save as pom.xml, and run mvn validate. Check your work against the expected result 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.

    build.gradle — plugins, dependencies, tasks
    // build.gradle (Groovy DSL) — code, not XML. Far less ceremony.
    
    plugins {
        id 'java'                       // adds compile, test, jar tasks
        id 'application'                // adds the 'run' task
    }
    
    group = 'com.mycompany'
    version = '1.0.0'
    
    repositories {
        mavenCentral()                  // where dependencies are downloaded from
    }
    
    dependencies {
        // implementation = compile scope. ONE line vs 7 of XML.
        implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'
    
        // testImplementation = test scope (only on the test classpath).
        testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
    }
    
    application {
        mainClass = 'com.mycompany.App' // entry point for './gradlew run'
    }
    
    tasks.named('test') {
        useJUnitPlatform()              // run tests with the JUnit 5 engine
    }
    This is a build.gradle (Groovy DSL), not runnable code. Save it at the project root and build with ./gradlew build. Each implementation line is one dependency.
    Run the Gradle wrapper and read the dependency tree
    # Always use the wrapper (./gradlew) committed in the repo, NOT a global
    # 'gradle' install — the wrapper pins the exact Gradle version for everyone.
    
    ./gradlew build          # compile + test + package  (= mvn package)
    ./gradlew test           # run the tests             (= mvn test)
    ./gradlew run            # run the application        (= mvn exec:java)
    ./gradlew clean          # delete build output        (= mvn clean)
    ./gradlew dependencies   # print the resolved graph   (= mvn dependency:tree)
    Output
    > Task :compileJava
    > Task :test
    BUILD SUCCESSFUL in 1s
    7 actionable tasks: 4 executed, 3 up-to-date
    
    # ./gradlew dependencies shows TRANSITIVE deps you never listed:
    compileClasspath
    \--- com.fasterxml.jackson.core:jackson-databind:2.17.0
         +--- com.fasterxml.jackson.core:jackson-annotations:2.17.0
         \--- com.fasterxml.jackson.core:jackson-core:2.17.0
    These are terminal commands run through the committed wrapper (./gradlew). The output shows the build succeeding and the transitive dependencies that one declared dependency pulled in.

    🎯 Your Turn #2 — Finish the build.gradle

    Add the repository and the two dependency configurations. The expected result is in the comments.

    Complete the Gradle dependencies block
    // build.gradle
    // 🎯 YOUR TURN — fill in the blanks marked with ___
    
    plugins {
        id 'java'
    }
    
    repositories {
        ___()        // 👉 the standard public repo: mavenCentral
    }
    
    dependencies {
        // A normal compile-time library:
        ___ 'org.apache.commons:commons-lang3:3.14.0'   // 👉 use: implementation
    
        // A test-only library:
        ___ 'org.junit.jupiter:junit-jupiter:5.10.2'    // 👉 use: testImplementation
    }
    
    // ✅ Expected: './gradlew build' downloads both jars from Maven Central and
    //    prints BUILD SUCCESSFUL. commons-lang3 is on the main classpath;
    //    junit-jupiter is on the test classpath only.
    Replace each ___, save as build.gradle, and run ./gradlew build. Check your work against the expected result 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.

    Challenge: wire up Gson + JUnit in Gradle
    // 🎯 MINI-CHALLENGE: add a JSON library to a Gradle build
    //
    // Starting from a build.gradle that already has the 'java' plugin and
    // mavenCentral():
    //   1. Add Google's Gson as a COMPILE dependency.
    //        coordinates -> com.google.code.gson:gson:2.11.0
    //   2. Add JUnit 5 as a TEST-ONLY dependency.
    //        coordinates -> org.junit.jupiter:junit-jupiter:5.10.2
    //   3. Run './gradlew dependencies' to confirm Gson resolved.
    //
    // ✅ Expected: BUILD SUCCESSFUL, and the dependency report lists
    //    com.google.code.gson:gson:2.11.0 on the compileClasspath.
    
    // your build.gradle dependencies { } block here
    Write the dependencies { ... } block yourself, then run ./gradlew dependencies to confirm Gson resolved. Compare against the expected result in the comments.

    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

    TaskMavenGradle
    Config filepom.xml (XML)build.gradle (code)
    Build (compile+test+jar)mvn package./gradlew build
    Run testsmvn test./gradlew test
    Install to local repomvn install./gradlew publishToMavenLocal
    Clean outputmvn clean./gradlew clean
    Compile dependency<scope>compile</scope>implementation '...'
    Test dependency<scope>test</scope>testImplementation '...'
    Dependency treemvn dependency:tree./gradlew dependencies

    Frequently Asked Questions

    What is the difference between Maven and Gradle?

    Both are build tools that compile, test, package, and manage dependencies for Java projects. Maven configures everything in an XML file (pom.xml) and favours strict conventions, so projects look the same everywhere. Gradle configures the build in real code (a Groovy or Kotlin script), which is more concise and flexible, and it is usually 2-10x faster on repeat builds thanks to incremental compilation, a build cache, and a long-running daemon.

    What are groupId, artifactId, and version (GAV coordinates)?

    They are the three values that uniquely identify any artifact in a repository like Maven Central. groupId is your organisation's reverse-domain namespace (com.mycompany), artifactId is the project name (my-app), and version is the release (1.0.0). Together they form a coordinate such as com.mycompany:my-app:1.0.0 that any other build can depend on.

    What is a transitive dependency?

    It is a dependency of your dependency that you never listed yourself. If you depend on jackson-databind, it pulls in jackson-core and jackson-annotations automatically, and those come in too. Both Maven and Gradle resolve this whole graph for you. Run 'mvn dependency:tree' or './gradlew dependencies' to see every transitive jar your build is actually using.

    What does a SNAPSHOT version mean, and how is it different from a release?

    A version ending in -SNAPSHOT (e.g. 1.2.0-SNAPSHOT) is an in-progress build that can change at any time, so the tool re-downloads the latest copy. A release version (1.2.0) is immutable: once published it never changes, so builds are reproducible. Depend on SNAPSHOTs only inside your own active development; never ship a release that depends on someone else's SNAPSHOT.

    Should I choose Maven or Gradle for a new project?

    Pick Maven when you want simplicity, stability, and a setup any Java developer instantly recognises — it is the enterprise default and great for straightforward libraries and services. Pick Gradle when build speed matters, when you have many modules, or when you need custom build logic — it is the standard for Android and a popular choice for new Spring Boot apps. Either way, generate the project from start.spring.io or 'mvn archetype:generate' rather than writing it by hand.

    🎉 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).

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.