Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Managing Dependencies and Build Lifecycles with Maven

Hello. In the previous lessons, you refreshed Java collections, Streams, and exception handling. Those are code-level skills; Maven is the project-level mechanism that makes the same code reproducible on another machine, in a CI pipeline, or in a teammate’s IDE.

This lesson focuses on two closely connected responsibilities: declaring the libraries a project needs and using Maven’s lifecycle to compile, test, package, and locally install a Java application. By the end, you should be able to read a typical pom.xml, choose suitable dependency scopes, and select the right Maven command for local development or a pre-push verification.


Maven’s role: a repeatable project contract

Maven is a build tool. Instead of manually downloading JAR files, configuring classpaths, choosing output folders, and remembering the order in which to compile and test, you describe the project in one central file: pom.xml.

The POM is short for Project Object Model. It records such things as:

  • the project’s identity and version;
  • the Java version and build settings;
  • external libraries and their scopes;
  • build plugins;
  • how Maven should package the application.

Maven then follows conventions. A standard project layout looks like this:

job-tracker-api/
├── pom.xml
├── mvnw
├── mvnw.cmd
├── src/
│   ├── main/
│   │   ├── java/
│   │   └── resources/
│   └── test/
│       ├── java/
│       └── resources/
└── target/              generated by Maven; do not commit it

The folders matter:

  • src/main/java contains production Java code.
  • src/main/resources contains runtime resources, such as Spring Boot’s application.properties.
  • src/test/java contains JUnit tests.
  • src/test/resources contains test-only files such as sample JSON or test configuration.
  • target contains generated output: compiled .class files, test reports, and packaged artifacts.
The Maven Lifecycles diagram shows the three built-in lifecycles. For routine Java and Spring Boot work, the Default lifecycle contains the familiar compile, test, package, verify, install, and deploy phases; the Clean lifecycle removes generated build output.

A useful distinction is that Maven is largely declarative. You declare, “this project uses JUnit” or “this project should produce a JAR.” Maven knows the conventional steps and plugins required to achieve that result.

Maven Getting Started Guide – Maven

Read Apache Maven’s official “Maven Getting Started Guide” to connect the POM, standard layout, dependency declarations, and generated output into one working model.

In “How do I make my first Maven project?”, read the POM overview. Focus on why the POM is the project’s central description and on the meanings of groupId, artifactId, and version. Then read the beginning of “How do I compile my application sources?” from the standard-layout explanation. Note the locations Maven expects for main code, test code, and compiled classes. Finally, in “How do I use external dependencies?”, read the dependency-coordinate discussion. Pay particular attention to what Maven needs in order to find a library.


Reading a POM and managing dependencies

Every Maven project has coordinates:

<groupId>io.github.yourname</groupId>
<artifactId>job-tracker-api</artifactId>
<version>0.0.1-SNAPSHOT</version>

Together, these identify the artifact Maven produces.

Element Meaning Example
groupId Organization or ownership namespace, conventionally a reversed domain io.github.yourname
artifactId Project and output-artifact name job-tracker-api
version Current project release version 0.0.1-SNAPSHOT

SNAPSHOT means the version is under active development. It is appropriate while building your portfolio application; it is not a claim that the project is production-released.

A typical dependency declaration has the same identifying information:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

When Maven needs this library, it first checks the local repository, normally located under your user home directory:

~/.m2/repository

If the dependency is absent locally, Maven downloads it from a configured remote repository, commonly Maven Central, and stores a cached copy locally. This explains why the first build after cloning a project is often slower than later builds.

Dependency scopes answer “when is this library needed?”

A scope does not simply mean “important” or “unimportant.” It controls where a dependency is available in the build and whether it belongs in the deployed runtime classpath.

Scope Available for Typical example
compile Main-code compilation, tests, and runtime; this is the default when scope is omitted A library directly used by application code
test Test compilation and test execution only JUnit, Mockito
runtime Tests and runtime, but not main-code compilation PostgreSQL JDBC driver when code relies on DataSource rather than PostgreSQL driver classes
provided Compilation and tests, but supplied by the deployment environment at runtime Servlet API in a traditional external servlet-container deployment

For a Spring Boot application, you will commonly see dependencies resembling:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Notice two practical details:

  1. A dependency can bring further dependencies. spring-boot-starter-web, for example, brings a curated set of libraries needed for web development. These indirect libraries are transitive dependencies.
  2. Spring Boot usually manages compatible versions for its starters. If the project uses the Spring Boot parent POM or dependency management, do not add an arbitrary version to every Spring dependency. Overriding a managed version without a concrete reason can create incompatibilities.

One important correction to a common assumption: the ordinary Maven JAR packaging does not automatically put every dependency inside a JAR. It normally produces a thin JAR that expects dependencies separately. Spring Boot’s packaging plugin commonly repackages the result into an executable JAR with the runtime dependencies arranged inside it.

If you need to inspect the full resolved dependency graph, run:

./mvnw dependency:tree

On Windows PowerShell, use:

.\mvnw.cmd dependency:tree

This is particularly useful when a dependency conflict, a vulnerable transitive library, or an unexpected version appears in a project.


Maven lifecycles: choose the last result you need

Maven has three built-in lifecycles:

  • Default builds, tests, packages, verifies, installs, and can deploy the application.
  • Clean removes output from earlier builds.
  • Site generates project documentation sites.

For backend development, you will work mainly with the Clean and Default lifecycles.

The central rule is:

When you call a Maven phase, Maven runs that phase and every earlier phase in the same lifecycle.

So mvn package does not merely create a JAR. Before packaging, Maven validates the project, compiles main code, prepares resources, compiles tests, and runs tests.

The main Default lifecycle phases to remember are:

Phase What it means in day-to-day work
validate Check that the project has the information Maven needs.
compile Compile production code from src/main/java.
test Compile and run unit tests from src/test/java.
package Create the distributable artifact, commonly a JAR.
verify Run additional checks that verify the package meets quality requirements.
install Put the built artifact in your local Maven repository for use by other local projects.
deploy Publish an artifact to a shared remote repository; normally handled by CI or release processes.

The Clean lifecycle is simpler:

Phase Effect
clean Removes generated files from the previous build, normally the target directory.

Introduction to the Build Lifecycle - Apache Maven

Read this Apache Maven guide for the exact meaning of lifecycle phases and the reason one command can perform several build steps.

In “Build Lifecycle Basics,” read the lifecycle definition, then study the listed Default lifecycle phases immediately below it. Focus on the distinction between a lifecycle and an individual phase. Next, in “Usual Command Line Calls,” read the guidance on verify. Notice why verify is often a better confidence check than stopping at package. Finally, in “Setting Up Your Project to Use the Build Lifecycle,” read the packaging explanation. Use the jar bindings table to see how plugins such as the compiler, Surefire test runner, and JAR plugin participate in lifecycle phases.

A phase is a named stage such as test or package. A plugin goal is a concrete task that runs within a phase.

For example, in a typical JAR project:

  • the Compiler plugin’s compile goal runs during the compile phase;
  • the Surefire plugin’s test goal runs during the test phase;
  • the JAR plugin’s jar goal runs during the package phase.

You usually invoke the phase, not individual plugin goals. Maven then uses the POM’s packaging type and plugin configuration to select the needed goals.


Commands you will actually use

Prefer the Maven Wrapper when a repository provides it. The wrapper uses the Maven version intended for that project and avoids relying on whichever Maven version happens to be installed globally.

Goal macOS/Linux Windows PowerShell When to use it
Confirm Maven and JDK ./mvnw -v .\mvnw.cmd -v First check after cloning or when Java versions seem wrong
Compile main code ./mvnw compile .\mvnw.cmd compile Quick compilation check
Run tests ./mvnw test .\mvnw.cmd test While implementing a focused change
Remove generated output and test ./mvnw clean test .\mvnw.cmd clean test Reliable local test run from a clean state
Package the application ./mvnw package .\mvnw.cmd package Create a JAR after tests pass
Verify the build ./mvnw clean verify .\mvnw.cmd clean verify Good final check before pushing a branch
Install locally ./mvnw install .\mvnw.cmd install When another local Maven project needs this artifact

If the project does not include a wrapper, replace ./mvnw with mvn.

What happens with common commands?

./mvnw test

Maven compiles production code if necessary, compiles tests, and runs the test suite. It does not create a JAR.

./mvnw package

Maven performs all earlier Default lifecycle work through package. If a test fails, packaging stops. A successful package usually appears under target/.

./mvnw clean package

Maven first runs the Clean lifecycle, removing stale output. It then runs the Default lifecycle through package. This helps catch problems that incremental compilation can occasionally conceal.

./mvnw clean verify

This is an excellent final local command before pushing your work. It builds from scratch, executes unit tests, creates the package, and runs any configured verification checks. Later, when you add integration tests, quality checks, and CI, this command becomes even more valuable.

./mvnw install

This creates the package and copies it into your local Maven repository. Another local Maven project can then depend on your artifact using its coordinates. For a typical standalone Spring Boot backend, you will use install less often than test, verify, and package.

./mvnw deploy

Do not treat deploy as a routine local command. It publishes an artifact to a shared repository and can require credentials. In professional projects, deployment is usually controlled through CI/CD workflows.

Maven Tutorial - Crash Course

Watch the lifecycle demonstration from “Maven Tutorial - Crash Course” by Marco Codes. It provides a useful terminal-level view of what changes in target when Maven cleans, compiles, tests, packages, and installs.

Start at clean through test. Watch how clean removes generated output, compile writes class files under target, and test compiles and executes test code. Then watch package and install. Focus on the difference between producing a JAR in target and installing that JAR into the local Maven repository.


A practical workflow for your backend projects

When you return to a Spring Boot repository, use this routine from the directory containing pom.xml.

  1. Check the project toolchain.

    ./mvnw -v
    

    Confirm the reported Java version is compatible with the project. If a project is configured for Java 21 but Maven reports Java 17, the problem is usually the JDK selected by JAVA_HOME, the IDE, or the terminal session.

  2. Let Maven establish the project dependencies.

    ./mvnw test
    

    The first run may download dependencies and plugins. Let it finish rather than interrupting it midway.

  3. Make a focused code change and rerun tests.

    ./mvnw test
    
  4. Before pushing a feature branch, verify from clean output.

    ./mvnw clean verify
    
  5. Package only when you need a runnable or distributable artifact.

    ./mvnw package
    

For a Spring Boot project, inspect target/ after packaging. Depending on the configured Spring Boot plugin, the JAR may be runnable with a command similar to:

java -jar target/job-tracker-api-0.0.1-SNAPSHOT.jar

Use the actual file name Maven generated rather than assuming it.

Interpreting failures without hiding them

Maven output can look noisy, but locate the first meaningful error, rather than only reading the final BUILD FAILURE line.

Failure type Likely cause First response
Compilation failure Invalid Java syntax, missing import, incompatible API, wrong Java release Read the source-file line number and compiler message
Test failure Production behavior does not match an assertion Read the failed test name and assertion details
Dependency-resolution failure Wrong coordinates, unavailable repository, network issue, proxy configuration Check the dependency declaration and the earliest download error
Java-version failure Maven runs with an incompatible JDK Run ./mvnw -v; compare its Java version with the POM configuration
Tests unexpectedly not found Test placement or naming does not follow Maven conventions Check src/test/java and use names such as SomethingTest

Do not make -DskipTests your normal workaround for a failing build. Skipping tests can be useful in a narrow situation, but it changes the meaning of a green build: it confirms compilation and packaging, not correctness.


Key takeaways

Maven turns a Java project into a reproducible build contract:

  • pom.xml declares project identity, dependencies, plugins, and build configuration.
  • Maven expects conventional directories such as src/main/java, src/test/java, and target.
  • Dependencies are identified by group, artifact, and version; scopes determine when they are available.
  • test dependencies belong to tests, while runtime dependencies such as a JDBC driver can be absent from main-code compilation.
  • Calling a lifecycle phase runs all earlier phases in that lifecycle.
  • Use clean test during development, clean verify as a strong pre-push check, and package when you need an artifact.
  • Prefer mvnw or mvnw.cmd when a project includes the Maven Wrapper.
  • Read Maven failures from the first useful error, rather than suppressing tests or treating BUILD FAILURE as the diagnosis.

Next, you will move from local project reliability to collaborative workflow: creating a focused Git feature branch, committing a coherent change, pushing it to GitHub, and opening a pull request.

Can't find a good explanation? Sign up and we'll make it for you

Sign up