0% completed
Build Tools: Maven and Gradle
On This Page
Coordinates and Repositories
Dependencies and the Tree They Form
Scopes: Which Classpath Gets a Library
The Maven Lifecycle
Gradle: Tasks Instead of Phases
Maven and Gradle Side by Side
Key Takeaways
Practice Questions
Between its source files and a running jar, the booking service needs four jobs done:
- Download the libraries it declares, and the libraries those libraries need.
- Compile the main code and the tests.
- Run the tests.
- Package everything into one executable jar.
A build tool does these jobs, and the two common ones for Java are Maven and Gradle. Each asks for a library in its own way. This is the request in pom.xml, the build file of Maven:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency>
This is the same request in build.gradle, the build file of Gradle:
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
Maven describes the build as XML data, while Gradle describes it as a short program. We built the booking service with both, and each produced an executable jar. The ideas underneath are shared: coordinates, dependencies, scopes, an ordered build, and a wrapper. The sections below explain those ideas, then compare the two tools.
Coordinates and Repositories
Every library is identified by three coordinates: a groupId for the organization, an artifactId for the library, and a version. They are often written joined by colons. The H2 database in the booking service is com.h2database:h2:2.4.240. The Gradle line above uses the same form, with the version left out. The Spring Boot plugin and the dependency-management plugin supply it from Spring Boot's list of managed versions.
A build tool downloads libraries from a repository, a server that stores published libraries by their coordinates. The usual one is Maven Central, and both tools use it. Downloaded files are kept in a local cache. On our machine, Maven stored them under ~/.m2/repository, and Gradle created its own folder, ~/.gradle, on its first run.
Dependencies and the Tree They Form
A library you declare usually depends on other libraries, called transitive dependencies, and the build tool resolves the whole tree. Our copy of the booking service has data access as well as web support. Maven printed its declared dependencies like this (the first level only):
+- org.springframework.boot:spring-boot-starter-webmvc:jar:4.1.2:compile +- org.springframework.boot:spring-boot-starter-data-jpa:jar:4.1.2:compile +- com.h2database:h2:jar:2.4.240:runtime +- org.springframework.boot:spring-boot-starter-data-jpa-test:jar:4.1.2:test \- org.springframework.boot:spring-boot-starter-webmvc-test:jar:4.1.2:test
The three declarations that are not test-only became 65 library jars inside the packaged application. The Spring Boot plugin added one more jar of its own, spring-boot-jarmode-tools, for 66 in total. Sometimes two parts of the tree ask for different versions of the same library. Maven then uses the version closest to your project in the tree, which its documentation calls the nearest definition.
The last word on each line is the scope, the subject of the next section.
Scopes: Which Classpath Gets a Library
A scope decides on which classpaths a dependency appears: compiling the main code, running it, or compiling and running tests. The classpath, as defined in What Spring Boot Is, is the list of libraries a program can load. Maven calls these sets scopes, and Gradle calls them configurations. They map closely:
| Maven scope | Gradle configuration | Compile main | Run main | Tests |
|---|---|---|---|---|
| compile | implementation | Yes | Yes | Yes |
| runtime | runtimeOnly | No | Yes | Maven yes, Gradle only when run |
| provided | compileOnly | Yes | No, supplied by the platform | Maven yes, Gradle no |
| test | testImplementation | No | No | Yes |
The documentation of each tool defines these sets. A dependency with Maven's runtime scope, for example, is not required for compilation but is required for execution, and Maven also puts it on the test classpath. Maven's provided scope expects the JDK or a container to provide the dependency at runtime, and Maven still adds it to the classpath for compiling and running tests. Gradle's compileOnly does not reach the tests, which use testImplementation and testCompileOnly instead.
The booking service shows why scopes matter. H2 has runtime scope, because no class of the service refers to H2 directly. Spring loads its driver when the application runs. The test starters have test scope. In the packaged jar we found h2-2.4.240.jar in BOOT-INF/lib, but no JUnit, AssertJ, or Mockito jar.
Notice the row for H2. It is missing from the compile column, so main code that imports an H2 class would not compile, yet it is present when the application runs.
The Maven Lifecycle
Maven's build is a lifecycle, an ordered list of phases, and asking for one phase runs every phase before it. The documentation lists the main phases in order: validate, compile, test, package, verify, install, and deploy. Plugin goals, small units of work that plugins provide, carry out each phase.
When we ran ./mvnw -B package on the booking service, Maven printed each goal as it ran:
[INFO] --- resources:3.5.0:resources (default-resources) @ booking --- [INFO] --- compiler:3.15.0:compile (default-compile) @ booking --- [INFO] --- resources:3.5.0:testResources (default-testResources) @ booking --- [INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ booking --- [INFO] --- surefire:3.5.6:test (default-test) @ booking --- [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] --- jar:3.5.1:jar (default-jar) @ booking --- [INFO] --- spring-boot:4.1.2:repackage (repackage) @ booking --- [INFO] BUILD SUCCESS
The order follows the phases. Resources are copied and code is compiled, then tests are compiled and run, and only then is the jar built. The last goal, repackage, comes from the Spring Boot plugin. It turns the plain jar into the executable jar from Running and Packaging an Application. Running ./mvnw -B compile instead stopped after the first two goals.
Notice that the tests run between compiling and packaging. A failing test stops the build, so no jar is produced from code whose tests fail.
Gradle: Tasks Instead of Phases
Gradle's build is a set of tasks, each of which declares the tasks it depends on. Gradle runs only the tasks needed for the one you ask for. Here is the build file the Initializr generated, without its group, version, and Java lines:
plugins { id 'java' id 'org.springframework.boot' version '4.1.2' id 'io.spring.dependency-management' version '1.1.7' } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-webmvc' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
The Spring Boot plugin adds a task named bootJar, which its documentation says builds executable jars. Our first ./gradlew build ran 13 tasks, among them compileJava, processResources, bootJar, compileTestJava, and test. It took 1 minute 30 seconds, which included the wrapper downloading Gradle 9.8.2. The jar appeared in build/libs instead of target. Gradle's summary counted 7 of the 13 as actionable tasks, the ones that do real work, and reported "7 actionable tasks: 7 executed".
A second run with no changes reported "7 actionable tasks: 7 up-to-date" and finished in 6 seconds, one run each on one laptop. The Gradle documentation explains the difference. Gradle tests whether a task's inputs or outputs changed since the last build, and if not, it skips the task. Gradle also keeps a background process, the daemon, between builds. When we repeated the unchanged build after the daemon had stopped, it took 1 minute 8 seconds, because the daemon had to start again.
Maven and Gradle Side by Side
| Topic | Maven | Gradle |
|---|---|---|
| Build file | pom.xml, XML data | build.gradle, a Groovy or Kotlin program |
| Unit of work | Phases, carried out by plugin goals | Tasks with dependencies between them |
| Declaring a dependency | A dependency element with a scope | One line in a configuration such as implementation |
| Output folder | target | build/libs |
| Wrapper | ./mvnw | ./gradlew |
| Repeated build, no changes | Runs the goals again | Skips up-to-date tasks |
Both tools work well with Spring Boot, and the Initializr generates either. Maven's fixed lifecycle makes every project look alike. Gradle's build files allow more custom logic, which also needs more discipline. This course uses Maven, because its output is easier to read in a lesson.
Key Takeaways
- A library is named by its coordinates, groupId, artifactId, and version, and is downloaded from a repository into a local cache.
- Declared dependencies bring transitive ones, and three declarations of the booking service became 65 of the 66 jars packaged into the application.
- Scopes and configurations decide which classpaths get a library, and test libraries never reach the packaged jar.
- Maven runs ordered phases through plugin goals, and the package phase runs the tests before building the jar.
- Gradle runs tasks with dependencies and skips the ones whose inputs have not changed.
That completes the Java and build background the service needs. Wiring Objects by Hand returns to the main method of the booking service and the cost of connecting objects without a container.
Practice Questions
1. A developer adds the PostgreSQL driver with compile scope, and a colleague suggests runtime scope instead. What does the change protect against?
<details><summary>Show answer</summary>With runtime scope, main code that imports a PostgreSQL class directly no longer compiles. The service talks to the database through standard interfaces, and the driver is needed only when the application runs. Runtime scope keeps it on the running classpath and in the jar, while keeping the code independent of one driver.
</details>2. The build server runs ./mvnw package, and one test fails. What does the build produce, and why?
It produces no new jar, and the build fails. The package phase runs every earlier phase in order, and the test phase comes before it. When a test fails, Maven stops before the jar and repackage goals, so code with a failing test is never packaged. A jar from an earlier build may still sit in target, so a build server should start from a clean folder.
</details>3. Two starters bring different versions of the same logging library, one at depth 2 of the tree and one at depth 3. Which version does Maven choose, and how can the team force a version?
<details><summary>Show answer</summary>Maven chooses the version at depth 2, the nearest definition. The team can force a version by declaring the library directly, which puts it at depth 1, or by setting it in dependency management. With the Spring Boot parent, the versions it manages usually settle such conflicts before they appear.
</details>4. A teammate says Gradle is faster than Maven because the second build took 6 seconds. What did that run actually show?
<details><summary>Show answer</summary>It showed that Gradle skipped every task because nothing had changed, not that compiling or testing is faster. All 7 actionable tasks were up-to-date. The time also depends on the daemon: the same unchanged build took 1 minute 8 seconds when the daemon had to start again. A fair comparison would change the same file in both projects and time several builds of each, and even then one laptop gives only one data point.
</details>Reading Progress
0%
On This Page
Coordinates and Repositories
Dependencies and the Tree They Form
Scopes: Which Classpath Gets a Library
The Maven Lifecycle
Gradle: Tasks Instead of Phases
Maven and Gradle Side by Side
Key Takeaways
Practice Questions