Every real Java project uses packages and a build tool to compile, manage dependencies, test and package.
A package groups related classes and gives them a namespace. Declare it at the top of the file (package com.example.billing;) and place the file in the matching directory, com/example/billing/. Package names are reversed domain names in lowercase. Use import to refer to classes from other packages by their simple name; java.lang is imported automatically.
At runtime the JVM finds classes on the classpath: a list of directories and JAR files (zip archives of .class files plus metadata). Managing that list by hand quickly becomes painful, which is why everyone uses a build tool.
Maven and Gradle both:
- download dependencies (and their transitive dependencies) from Maven Central by coordinates
groupId:artifactId:version, - use the standard layout:
src/main/java,src/main/resources,src/test/java, - compile, run tests and package a JAR with one command.
Maven is configured declaratively in pom.xml and runs lifecycle phases: mvn package compiles, tests and builds the JAR into target/. Gradle uses a Kotlin or Groovy build script (build.gradle.kts) and tasks: ./gradlew build puts output in build/. Both projects normally commit a wrapper (mvnw, gradlew) so everyone uses the same tool version.
In Maven, dependency scope matters: test dependencies like JUnit are available only to tests. In Gradle the equivalent is testImplementation versus implementation.
Set the Java version explicitly — <maven.compiler.release>21</maven.compiler.release> in Maven, or a Java toolchain in Gradle — so builds don't depend on whichever JDK happens to be installed.
Example
// File: src/main/java/com/example/billing/Invoice.java
package com.example.billing;
import java.math.BigDecimal;
import java.util.List;
public record Invoice(String id, List<BigDecimal> lines) {
public BigDecimal total() {
return lines.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// pom.xml (excerpt):
// <properties><maven.compiler.release>21</maven.compiler.release></properties>
// <dependency>
// <groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId>
// <version>5.11.0</version><scope>test</scope>
// </dependency>
// Build and test: ./mvnw package or ./gradlew buildRemember
- Package names mirror directories:
com.example.app→com/example/app/ - Build tools resolve dependencies by group:artifact:version and use a standard layout
- Commit the wrapper and pin the Java release for reproducible builds
Watch out for
- Putting a file in a directory that doesn't match its package declaration, which leads to "class not found" errors.