Maven Basics: pom.xml

Read and write a pom.xml: coordinates, the standard folder layout, the Java release setting and pinned plugin versions.

Syntax<groupId>com.example</groupId> <artifactId>orders-service</artifactId> <version>1.0.0-SNAPSHOT</version> <properties><maven.compiler.release>25</maven.compiler.release></properties>

Maven builds a Java project from one file, pom.xml (Project Object Model), at the root of the project. Instead of a script of steps, the POM describes the project: what it is called, what it depends on and which plugins build it. Maven supplies the steps by convention, so most of a small service's POM is identity and dependencies.

Coordinates

Every artifact is named by three coordinates. groupId is the organisation or product, usually a reversed domain such as com.example. artifactId is this project, such as orders-service. version is the release. A version ending in -SNAPSHOT means work in progress: Maven may fetch a newer build of a snapshot from a remote repository, while a release version is treated as never changing. Ship release versions to production, never snapshots.

packaging defaults to jar. The built file lands in target/ as artifactId-version.jar, for example target/orders-service-1.0.0-SNAPSHOT.jar.

The standard layout

Maven expects production code in src/main/java, files such as application.properties in src/main/resources, and tests in src/test/java with their resources in src/test/resources. Everything the build produces goes into target/, which belongs in .gitignore. Follow the layout and almost no configuration is needed; fight it and every plugin needs telling where things are.

Setting the Java version

Set maven.compiler.release to 25. This passes --release 25 to javac, which fixes both the language level and the set of JDK APIs you may call. The older source and target pair only sets the language level and class file version, not the API, so code built on a newer JDK can call an API that does not exist on the older runtime and fail with NoSuchMethodError in production. Also set project.build.sourceEncoding to UTF-8, or the build depends on the machine's default encoding.

Inheritance and the effective POM

Every POM inherits from Maven's built-in super POM, and possibly from a parent such as spring-boot-starter-parent. What actually runs is the merge of all of them. mvn help:effective-pom prints that merged result, which is the first thing to look at when a setting seems to be ignored.

Pin plugin versions

The compiler, test and packaging plugins have default versions that depend on which Maven release you run. Two developers on different Maven versions can then get different builds. Pin each plugin's version in <pluginManagement> (or inherit pinned versions from a parent), and use the Maven Wrapper so everyone runs the same Maven too.

Downloaded dependencies and plugins are cached in ~/.m2/repository. Remote artifacts come from Maven Central unless you configure another repository, which companies often do through a proxy such as Nexus or Artifactory set up in ~/.m2/settings.xml.

Example

Example · xml
<?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>

  <!-- Coordinates: together they name this artifact everywhere -->
  <groupId>com.example</groupId>
  <artifactId>orders-service</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.release>25</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.junit</groupId>
        <artifactId>junit-bom</artifactId>
        <version>6.0.3</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

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

  <build>
    <!-- Pin plugin versions so every machine builds the same way -->
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.15.0</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>3.5.6</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.5.1</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

When to use it

  • Set up a new orders API so it compiles with --release 25 and builds the same JAR on every developer machine and in CI.
  • Debug why a Spring Boot service compiles with the wrong Java level by reading the effective POM.
  • Publish an internal client library with stable coordinates that other services add as a dependency.

More examples

The standard project layout

Package folders under src/main/java mirror the package names, and resources are copied to the root of target/classes. Code reads them from the class path, not from src/main/resources.

Example · text
orders-service/
  pom.xml
  mvnw, mvnw.cmd, .mvn/          # Maven Wrapper (commit these)
  src/
    main/
      java/com/example/orders/OrderService.java
      resources/application.properties
    test/
      java/com/example/orders/OrderServiceTest.java
      resources/orders.json
  target/                        # build output, never committed
    classes/
    test-classes/
    orders-service-1.0.0-SNAPSHOT.jar

Everyday Maven commands

Each command runs every earlier step of the build too, so package also compiles and tests. Check -v first when a build behaves differently on CI: it often runs a different JDK.

Example · bash
./mvnw -v                    # Maven and JDK versions actually in use
./mvnw compile               # compile src/main/java into target/classes
./mvnw test                  # compile and run unit tests
./mvnw package               # build target/orders-service-1.0.0-SNAPSHOT.jar
./mvnw clean package         # delete target/ first, then package

# See the merged POM, including everything inherited
./mvnw help:effective-pom -Doutput=effective-pom.xml

Inheriting from the Spring Boot parent

The parent supplies pinned plugin versions, UTF-8 encoding and managed dependency versions, so the child POM stays short. The empty relativePath tells Maven not to look for the parent in the folder above.

Example · xml
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.1.1</version>
  <relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>orders-service</artifactId>
<version>1.0.0-SNAPSHOT</version>

<properties>
  <!-- The Boot parent turns this into the compiler's release setting -->
  <java.version>25</java.version>
</properties>

Discussion

  • Be the first to comment on this lesson.