Skip to content

11 - Packages, Modules, Maven

What this session is

About an hour. You'll learn how Java code is organized - packages (folder of related classes), modules (collection of packages with controlled visibility), and Maven (the build tool that manages your dependencies, compiles your code, and runs your tests). You'll publish a small library to your local Maven cache and consume it from another project.

Packages: organizing classes into folders

A package is a folder of classes that work together. Every Java file declares which package it belongs to:

package com.example.greet;

public class Hello {
    public static String say(String name) {
        return "Hello, " + name;
    }
}

That file lives at src/main/java/com/example/greet/Hello.java - the folder structure must match the package name.

To use it from another package:

package com.example.app;

import com.example.greet.Hello;

public class Main {
    public static void main(String[] args) {
        System.out.println(Hello.say("Alice"));
    }
}

Convention: package names are lowercase, dotted, reverse-domain-style (com.companyname.product.subsystem). Avoids name collisions across libraries from different organizations.

Access modifiers (revisited)

You met public and private in page 05. The full list:

Modifier Visible from
public Anywhere
protected Same package + subclasses (even in other packages)
(none - "package-private") Same package only
private Same class only

Package-private (no modifier) is the default and underused. Use it for helpers that should only be visible within a single package - internal collaboration, not the public API.

Modules: introduced in Java 9, optional

Modules group packages and control visibility between modules. They're declared in module-info.java at the root of a module:

// src/main/java/module-info.java
module com.example.greetapp {
    requires java.base;       // implicit, but you can be explicit
    requires java.sql;        // depending on the JDK's sql module
    exports com.example.greet;     // make this package visible to others
}

The honest assessment in 2026: most applications don't use modules. The Java standard library is modular (you'll see things like java.base, java.sql, java.net.http), but application code generally lives on the classpath without module-info.java. You'll meet modules when working with:

  • Custom JRE images via jlink (ship a tiny JVM with only the modules you need).
  • Native images via GraalVM.
  • Some libraries that ship as proper modules.

For your first contribution, you almost certainly don't need to write module-info.java. Recognize it when you see it.

Maven: the build tool

You set up a Maven project in page 10. Let's understand what's actually happening.

The pom.xml

The project's manifest. Key sections:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
    <modelVersion>4.0.0</modelVersion>

    <!-- Identity -->
    <groupId>com.example</groupId>
    <artifactId>mathutils</artifactId>
    <version>1.0.0</version>

    <!-- Build settings -->
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
    </properties>

    <!-- Dependencies -->
    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.11.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
  • groupId - your organization (reverse-domain). For personal projects, anything works (com.example, io.github.yourname).
  • artifactId - the project's name.
  • version - semver typically (1.0.0, 1.0.0-SNAPSHOT for in-development).
  • <dependencies> - what you depend on. Each <dependency> has groupId + artifactId + version (and optional scope).

The combination <groupId>:<artifactId>:<version> uniquely identifies an artifact in Maven Central (the public repository).

Common Maven commands

Command What it does
mvn compile Compile main code (but not tests).
mvn test Run tests.
mvn package Build a JAR in target/.
mvn install Install the JAR to your local Maven repo (~/.m2/repository/).
mvn clean Delete target/.
mvn dependency:tree Show your dependency tree (incl. transitive deps).
mvn versions:display-dependency-updates Check for newer versions of deps.

mvn package is the daily driver. mvn install lets other local projects pull your library.

Dependency scopes

A dependency's <scope> controls when it's available:

  • compile (default) - available everywhere.
  • test - only when compiling/running tests. (JUnit goes here.)
  • provided - needed to compile but supplied by the runtime (servlet APIs, for example).
  • runtime - needed at runtime but not compile time (JDBC drivers).

Stick to compile and test for most things.

Using Maven Central

Maven Central (central.sonatype.com) is the public repository of Java libraries. You declare a dependency in your pom.xml; Maven downloads it on first build.

A real example - add JSON support via Jackson:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.18.0</version>
</dependency>

After mvn install or any build, Jackson is downloaded to ~/.m2/repository/ and available to import:

import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonDemo {
    public static void main(String[] args) throws Exception {
        var mapper = new ObjectMapper();
        var json = mapper.writeValueAsString(java.util.Map.of("name", "Alice", "age", 30));
        System.out.println(json);    // {"name":"Alice","age":30}
    }
}

Search Maven Central by artifact name; copy the <dependency> snippet; paste into your pom; rebuild.

A small multi-module example

Let's build two interacting projects: a library and an app that uses it.

Project 1 - the library:

mvn archetype:generate -DgroupId=com.example -DartifactId=greetlib \
    -DarchetypeArtifactId=maven-archetype-quickstart \
    -DarchetypeVersion=1.4 -DinteractiveMode=false
cd greetlib

Edit src/main/java/com/example/Hello.java (rename if needed):

package com.example;

public class Hello {
    public static String say(String name) {
        return "Hello, " + name;
    }
}

Install to your local Maven repo:

mvn clean install

Project 2 - the app:

cd ..
mvn archetype:generate -DgroupId=com.example -DartifactId=greetapp ...
cd greetapp

Add the library as a dependency in pom.xml:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>greetlib</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

Use it:

package com.example;

import com.example.Hello;

public class Main {
    public static void main(String[] args) {
        System.out.println(Hello.say("Alice"));
    }
}
mvn compile
mvn exec:java -Dexec.mainClass=com.example.Main

You've now consumed your own library from another project, the same way you'd consume Jackson.

Gradle (alternative to Maven)

Gradle is the other major build tool. Uses Groovy or Kotlin DSL instead of XML:

plugins { id("java") }

dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.18.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
}

Faster than Maven for incremental builds; more flexible; harder to learn. Most Android projects use Gradle. JVM server projects use either (Maven is more common in enterprise).

Pick the one your target project uses. For learning, Maven is the gentler intro.

Exercise

Set up a real Maven project that uses an external library.

  1. Start a new Maven project (mvn archetype:generate ...).
  2. Add Jackson (com.fasterxml.jackson.core:jackson-databind:2.18.0) as a dependency.
  3. Write a Main.java that:
  4. Creates a Person record (or class) with name and age.
  5. Serializes it to JSON with ObjectMapper.writeValueAsString.
  6. Deserializes the JSON back to a Person with ObjectMapper.readValue.
  7. Prints both.
  8. Run with mvn compile exec:java -Dexec.mainClass=com.example.Main.
  9. Stretch: add a list of three Persons; serialize the whole list; deserialize back. (You'll need TypeReference<List<Person>> - search Stack Overflow for "Jackson List deserialize".)

What you might wonder

"Why does Maven download so many files on the first build?" It's pulling Jackson + all of Jackson's transitive dependencies. Stored in ~/.m2/repository/; shared across all your projects.

"What's the difference between <scope>compile and <scope>provided?" compile - bundled in the final JAR. provided - present at compile time but the runtime provides it (e.g., servlet API). Use provided for things like servlet libs; use compile for everything else.

"What's a Maven parent POM?" A pom.xml that other POMs inherit from (via <parent>). Used for multi-module projects to share config. Out of scope for one-module learning.

"Should I use the archetype:generate quickstart or start from scratch?" Either works. The quickstart gives you a working POM and folder structure to mutate. Some people prefer hand-rolling - fine if you know what to type.

"What's Maven Wrapper (mvnw)?" A script that downloads the right Maven version automatically. Many projects ship mvnw so users don't need to install Maven globally. If a project has mvnw, use ./mvnw instead of mvn.

Done

You can now: - Organize classes into packages with package declarations. - Distinguish access modifiers (public, protected, package-private, private). - Recognize modules (module-info.java) without needing to use them. - Write and edit a pom.xml. - Add dependencies from Maven Central. - Install your own library locally and consume it from another project. - Recognize Gradle as the alternative.

You've covered everything you need to read and write real Java codebases. Remaining pages: applying this to OSS contribution.

Next: Reading other people's code →

Comments