Packages

A Java package is a named group of related classes and interfaces, declared with a package statement at the top of each source file; the package name is part of every class's full name and mirrors the folder in which the files are stored. Packages prevent name clashes (two libraries can both have a class Tariff) and, with the access modifiers, decide what code outside the package may use (see Classes and Objects). They are the Java counterpart of Python packages and C# namespaces, with one difference: in Java the folder structure must match.

Package names are written in lowercase and, by convention, start with the organization's internet domain reversed, so they are unique worldwide: com.kestrel.billing for Kestrel Energy's billing code. A small project looks like this:

billing/
└── src/
    └── com/
        └── kestrel/
            └── billing/
                ├── Main.java
                └── Tariff.java
package com.kestrel.billing;

import java.math.BigDecimal;
import java.math.RoundingMode;

public record Tariff(String name, BigDecimal pricePerKwh) {
    public BigDecimal priceFor(int kwh) {
        return pricePerKwh.multiply(BigDecimal.valueOf(kwh)).setScale(2, RoundingMode.HALF_UP);
    }
}
package com.kestrel.billing;

import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        Tariff day = new Tariff("Day", new BigDecimal("0.27"));
        System.out.println("C-10042 owes EUR " + day.priceFor(412));
    }
}

The full name of the class is now com.kestrel.billing.Main. Main uses Tariff without an import because both are in the same package. Classes without a package line are in the unnamed package, which is fine for experiments, like the single-file examples on this site, but not for real code. Compact source files (void main() without a class) always live in the unnamed package and may not declare one.

Imports

An import lets you write a class's short name instead of its full name. It does not load or include anything; it only tells the compiler what the short name means. The package java.lang (String, Math, System, IO) is always imported. A static import brings in static members, so you can write max(…) instead of Math.max(…):

import java.math.BigDecimal;
import java.time.LocalDate;
import static java.lang.Math.max;

public class Main {
    public static void main(String[] args) {
        java.util.List<String> meters = java.util.List.of("E-001", "E-002");   // fully qualified, no import
        BigDecimal price = new BigDecimal("0.27");                              // imported
        LocalDate due = LocalDate.of(2026, 11, 4);                               // imported
        String team = "NOC";                                                     // java.lang: always available
        System.out.println(meters + " " + price + " " + due + " " + team);
        System.out.println("highest reading: " + max(412, 388));                // static import
    }
}
[E-001, E-002] 0.27 2026-11-04 NOC
highest reading: 412

A wildcard import such as import java.util.*; imports every class of one package (not of its subpackages: a package java.util.concurrent is a separate package despite the name). Two wildcards can make a name ambiguous. Both java.util and java.awt contain a type called List:

import java.util.*;
import java.awt.*;

public class Main {
    public static void main(String[] args) {
        List<String> teams = new ArrayList<>();
        System.out.println(teams);
    }
}
Main.java:6: error: reference to List is ambiguous

A single-type import always wins over a wildcard, so one extra line settles it:

import java.awt.*;
import java.util.*;
import java.util.List;   // a single-type import wins over both wildcards

public class Main {
    public static void main(String[] args) {
        List<String> teams = new ArrayList<>(List.of("NOC", "Billing"));
        System.out.println(teams);
    }
}
[NOC, Billing]

Compiling and running: the classpath

javac -d out compiles the sources and writes the class files into the folder out, in subfolders that match the packages. To run the program you give the full class name and tell the JVM where to look:

javac -d out src/com/kestrel/billing/*.java
find out -type f
java -cp out com.kestrel.billing.Main
out/com/kestrel/billing/Main.class
out/com/kestrel/billing/Tariff.class
C-10042 owes EUR 111.24

The classpath is the list of folders and JAR files in which the JVM and the compiler look for classes, set with the option -cp (or -classpath, or the environment variable CLASSPATH); entries are separated by : on Linux and macOS and by ; on Windows. When a class is not on it, the error depends on when the JVM notices:

java -cp wrong com.kestrel.billing.Main
Error: Could not find or load main class com.kestrel.billing.Main
Caused by: java.lang.ClassNotFoundException: com.kestrel.billing.Main

Here the main class itself is missing. When the main class is found but a class it needs is gone, for example because a JAR is missing from the classpath after a deployment, the program starts and fails at the first line that uses it:

Exception in thread "main" java.lang.NoClassDefFoundError: com/kestrel/billing/Tariff
	at com.kestrel.billing.Main.main(Main.java:7)
Caused by: java.lang.ClassNotFoundException: com.kestrel.billing.Tariff

ClassNotFoundException is also what code gets when it looks a class up by name at run time, as frameworks and JDBC drivers do:

public class Main {
    public static void main(String[] args) throws Exception {
        Class<?> found = Class.forName("java.time.LocalDate");
        System.out.println("found " + found.getName());
        Class.forName("com.kestrel.billing.TariffEngine");
    }
}
found java.time.LocalDate
Exception in thread "main" java.lang.ClassNotFoundException: com.kestrel.billing.TariffEngine

JAR files

A JAR file (Java Archive) is a ZIP archive that holds compiled class files and resources, plus a manifest file META-INF/MANIFEST.MF with metadata; it is the usual way to ship a Java library or application. The jar tool of the JDK creates one:

jar --create --file billing.jar --main-class com.kestrel.billing.Main -C out .
jar --list --file billing.jar
java -jar billing.jar
META-INF/
META-INF/MANIFEST.MF
com/
com/kestrel/
com/kestrel/billing/
com/kestrel/billing/Main.class
com/kestrel/billing/Tariff.class
C-10042 owes EUR 111.24

The manifest that --main-class produced:

Manifest-Version: 1.0
Created-By: 25.0.4.1 (Eclipse Adoptium)
Main-Class: com.kestrel.billing.Main

java -jar reads the Main-Class header to know where to start; a JAR without it fails with no main manifest attribute, in billing.jar. With -jar, the -cp option is ignored; extra JARs are listed in a Class-Path header in the manifest instead. Because a JAR is a ZIP file, unzip -l works on it too, which is handy on a server without a JDK.

Kinds of JAR files you meet in practice:

  • Library JARs, such as jackson-databind-2.22.3.jar: classes for other programs to use, no main class.
  • Executable "fat" JARs (also called uber JARs): the application plus all its dependencies in one file. Spring Boot builds such a JAR, which is why many services are started with just java -jar service.jar.
  • WAR files (web application archives): the same ZIP format with a different layout, deployed into an application server such as Apache Tomcat.

The module system

Packages organize classes, but before Java 9 nothing organized packages: every public class on the classpath could be used by everyone, and the JVM only found out at run time that a JAR was missing. The Java Platform Module System (JPMS, Java 9) adds modules: named groups of packages with a descriptor, module-info.java, that declares which other modules the module requires and which of its packages it exports; packages that are not exported cannot be used from outside, even if their classes are public.

module com.kestrel.billing {
    requires java.sql;
    exports com.kestrel.billing;
}

This module uses JDBC from the JDK module java.sql and offers its package com.kestrel.billing to other modules. Its second package, com.kestrel.billing.internal, stays private. Another module that requires it and imports from the internal package does not compile:

mods/com.kestrel.portal/com/kestrel/portal/App.java:4: error: package com.kestrel.billing.internal is not visible
import com.kestrel.billing.internal.RateCache;
                          ^
  (package com.kestrel.billing.internal is declared in module com.kestrel.billing, which does not export it)
1 error

Modules are found on the module path (--module-path) instead of the classpath. The JVM checks at start-up that every required module is present, so a missing component fails immediately instead of halfway through a request:

java --module-path mout --describe-module com.kestrel.billing
com.kestrel.billing file:///…/mout/com.kestrel.billing/
exports com.kestrel.billing
requires java.base mandated
requires java.sql
contains com.kestrel.billing.internal

The modular JDK

Most application code still runs on the classpath, in the unnamed module. The biggest effect of the module system is on the JDK itself, which consists of about 70 modules (java --list-modules lists 69 on Temurin 25). A program can ask which module a class belongs to:

public class Main {
    public static void main(String[] args) {
        System.out.println("String:     " + String.class.getModule().getName());
        System.out.println("HttpClient: " + java.net.http.HttpClient.class.getModule().getName());
        System.out.println("Connection: " + java.sql.Connection.class.getModule().getName());
        System.out.println("Main named? " + Main.class.getModule().isNamed());
        System.out.println("jdk.httpserver present? " + ModuleLayer.boot().findModule("jdk.httpserver").isPresent());
    }
}
String:     java.base
HttpClient: java.net.http
Connection: java.sql
Main named? false
jdk.httpserver present? true

Two consequences that operations people run into:

  • Strong encapsulation. Since JDK 17 (JEP 403), code can no longer reach into the internals of the JDK with reflection. Older libraries that did so fail with an InaccessibleObjectException, and start scripts sometimes contain options such as --add-opens java.base/java.lang=ALL-UNNAMED to open a package again. Each such option is a sign of a library that should be upgraded.
  • Smaller runtimes. jlink builds a custom runtime with only the modules an application needs. jdeps --print-module-deps tells you which those are. A runtime with java.base and java.sql (which pulls in java.logging, java.transaction.xa and java.xml) took 71 MB on our test machine, against 303 MB for the full JDK, which is useful for container images.
public class Main {
    public static void main(String[] args) throws Exception {
        var field = String.class.getDeclaredField("value");
        try {
            field.setAccessible(true);
            System.out.println("opened");
        } catch (RuntimeException e) {
            System.out.println(e.getClass().getName());
            System.out.println(e.getMessage().contains("does not \"opens java.lang\" to unnamed module"));
        }
    }
}
java.lang.reflect.InaccessibleObjectException
true

The full message reads Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @71a794e5; the part after @ differs on every run, which is why the example only tests for the stable part.

Module import declarations

Java 25 added import module (JEP 511), which imports all exported packages of a module at once: import module java.base; makes List, LocalDate, BigDecimal and the rest available. Compact source files get exactly this import automatically. As with wildcards, two modules can clash, and a single-type import resolves it:

import module java.base;
import module java.desktop;

public class Main {
    public static void main(String[] args) {
        List<String> teams = new ArrayList<>();
        System.out.println(teams);
    }
}
Main.java:6: error: reference to List is ambiguous
import module java.base;
import module java.desktop;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> teams = new ArrayList<>(List.of("NOC", "Billing"));
        System.out.println(teams + " " + LocalDate.of(2026, 10, 5));
    }
}
[NOC, Billing] 2026-10-05

Why build tools

javac and jar are enough for one folder of code. A real service has dozens of libraries, each with its own dependencies, tests that must run before every release, and a build that must give the same result on every developer's laptop and in the CI/CD pipeline. A build tool takes care of that: it downloads the right versions of all libraries, compiles, runs the tests and packages the result, all from one description of the project. Java has two dominant build tools, Maven and Gradle. Both download libraries from the same place: Maven Central, the public repository of open-source Java libraries, where each library is identified by its coordinates groupId:artifactId:version, for example com.fasterxml.jackson.core:jackson-databind:2.22.3.

Maven

Maven (Apache Maven) is a build tool for Java that describes a project declaratively in an XML file, pom.xml (Project Object Model): its coordinates, its dependencies and its plugins. It builds every project the same way through a fixed sequence of phases. As of October 2026 the recommended version is Maven 3.10.0; Maven 4 is still a release candidate (4.0.0-rc-7) and not meant for production yet.

<?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>

  <groupId>com.kestrel</groupId>
  <artifactId>billing-service</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

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

  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.22.3</version>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.6.0</version>
      </plugin>
    </plugins>
  </build>
</project>

maven.compiler.release compiles for Java 25, the test scope keeps JUnit out of the shipped application, and the Surefire plugin, which runs the tests, is pinned to a fixed version (plugins should be pinned just like libraries; 3.6.0 is the latest as of October 2026). Maven expects a standard folder layout, so the POM does not have to list source folders:

billing-service/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/com/kestrel/billing/   application code
│   │   └── resources/                  configuration files, templates
│   └── test/
│       └── java/com/kestrel/billing/   tests
└── target/                             build output (never commit it)

The build lifecycle

Maven builds in phases: validate, compile, test, package, verify, install, deploy. Asking for one phase runs all phases before it, so mvn package compiles, runs the tests, and only then builds the JAR:

mvn clean package          # delete target/, then compile, test and build the JAR
mvn verify                 # also run integration tests and other checks
mvn install                # copy the JAR into ~/.m2/repository for other local projects
./mvnw package             # the same with the Maven Wrapper: the project's own Maven version
[INFO] --- surefire:3.6.0:test (default-test) @ billing-service ---
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
[INFO] Building jar: /home/ravi/billing-service/target/billing-service-1.0.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Downloaded libraries are cached in the local repository ~/.m2/repository, so the second build needs no network. Companies usually put a repository manager in between (such as Sonatype Nexus or JFrog Artifactory) that mirrors Maven Central and hosts internal libraries. The Maven Wrapper (mvnw), a small script committed with the project, downloads the Maven version the project was written for, so builds do not depend on what is installed on the build server.

Scope Needed to compile Shipped with the application Typical use
compile (default) yes yes libraries the code uses
provided yes no, the server supplies it the servlet API in a WAR
runtime no yes a JDBC driver
test only the tests no JUnit, Mockito

Transitive dependencies

A transitive dependency is a library your project uses indirectly: you declare a dependency on library A, A depends on B, so the build tool downloads B and puts it on the classpath too, without B appearing in your own build file. The pom above declares two libraries; the build uses many more. mvn dependency:tree shows them all:

[INFO] com.kestrel:billing-service:jar:1.0.0
[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.22.3:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile
[INFO] |  \- com.fasterxml.jackson.core:jackson-core:jar:2.22.3:compile
[INFO] \- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO]    +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
[INFO]    |  +- org.opentest4j:opentest4j:jar:1.3.0:test
[INFO]    |  +- org.junit.platform:junit-platform-commons:jar:6.1.3:test
[INFO]    |  +- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO]    |  \- org.jspecify:jspecify:jar:1.0.0:test
[INFO]    +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test
[INFO]    \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
[INFO]       \- org.junit.platform:junit-platform-engine:jar:6.1.3:test

When two paths bring in different versions of the same library, Maven picks the one nearest to your project in the tree (and the first one declared when they are equally near); Gradle picks the highest version. Either way the result can be a version nobody chose on purpose. Declaring the version yourself, in Maven in a <dependencyManagement> section or with a BOM (bill of materials, a POM that only lists matching versions), makes the choice explicit.

Gradle

Gradle is a build tool for the JVM whose build scripts are code, in Kotlin (build.gradle.kts) or Groovy (build.gradle), organized as tasks; it uses the same Maven repositories and coordinates, and is known for fast, incremental builds. It is the standard build tool for Android apps and common for back-end services. As of October 2026 the current version is Gradle 9.8.0, which runs on Java 17 to 27.

plugins {
    application
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.fasterxml.jackson.core:jackson-databind:2.22.3")
    testImplementation(platform("org.junit:junit-bom:6.1.3"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(25)
    }
}

application {
    mainClass = "com.kestrel.billing.Main"
}

tasks.test {
    useJUnitPlatform()
}

The ideas map one to one on Maven: implementation is roughly the compile scope, testImplementation the test scope, and platform(…) imports a BOM, so the JUnit libraries need no version of their own. The toolchain setting makes Gradle compile with a JDK 25 even when Gradle itself runs on another version. Gradle projects are built with the Gradle Wrapper, ./gradlew, committed with the project:

./gradlew build                                      # compile, test, package
./gradlew test                                       # only the tests
./gradlew run                                        # start the application
./gradlew dependencies --configuration runtimeClasspath   # the dependency tree
Maven Gradle
Build file pom.xml (XML, declarative) build.gradle.kts or build.gradle (code)
Model fixed lifecycle of phases graph of tasks
Wrapper ./mvnw ./gradlew
Output folder target/ build/
Version conflicts nearest wins highest wins

Neither is better for every project. Maven builds are predictable and look alike everywhere; Gradle is more flexible and usually faster on big projects. Use whatever the project already uses.

Keeping dependencies safe

Most code in a Java service is not written by its team: it comes in through dependencies, and most of those are transitive. That is where many serious vulnerabilities live. The best-known case is Log4Shell (CVE-2021-44228, published 10 December 2021, CVSS score 10.0, the maximum). In the popular logging library Apache Log4j 2 (versions 2.0-beta9 up to 2.14.1), a special lookup syntax inside a logged message made the library fetch and run code from a remote server. Any text an attacker could get into a log, such as a user name or a browser header, was enough. Fixed versions came within days (2.15.0, then 2.16.0 and backports 2.12.2 and 2.3.1).

Dependency tree: billing-service directly depends on jackson-databind and on an internal library meter-client. meter-client depends on log4j-core, which is therefore a transitive dependency of billing-service that does not appear in its own build file. billing-service direct direct jackson-databind 2.22.3 meter-client 1.4 transitive log4j-core 2.14.1 not in billing-service's own build file
The vulnerable library is two levels down: the team never chose it, and it does not appear in their pom.xml. Only the full dependency tree shows it.

Many teams first had to find out whether they used Log4j at all, because it came in through other libraries and products. The lessons, in short:

  • Know what you run. Keep an inventory per application: the dependency tree, or better a software bill of materials (SBOM), which build plugins can generate on every build.
  • Scan continuously. Software composition analysis in the pipeline and tools such as Dependabot compare your dependencies with databases of known vulnerabilities.
  • Patch libraries, not just the JDK. Updating Java does not update the JARs inside an application. A fix means a new build of the application.
  • Pin versions. Fixed versions (no ranges), pinned plugins and wrappers make builds repeatable, so you know exactly what is in production. Gradle can also write lock files.
  • Retire what is dead. Log4j 1.x has had no fixes since 2015, yet still turns up in old applications.

See Vulnerability Management for how such findings are prioritized and handled.