Packages
Give every class a unique full name, lay out source folders to match, and group a service by feature so its internals can stay hidden.
package com.example.shop.orders; // first statement in the file
import java.util.List;
public class OrderService { } // full name: com.example.shop.orders.OrderServiceA package is a named group of types. It does three jobs at once: it gives every class a unique full name (com.example.shop.orders.OrderService), it sets an access boundary, and it decides where the compiled .class file lives on disk.
Declaring one
The package line must be the first statement in the file, before any import; only comments may come before it. Names are lowercase and start from a domain you control, reversed: com.example, io.acme.billing. That convention is how two libraries can both ship a class called Order without a clash.
The folders must match the name. com.example.shop.orders.OrderService compiles to com/example/shop/orders/OrderService.class, and the class loader looks for it at exactly that path below each class path root. Maven and Gradle expect the sources in the same shape under src/main/java. The source launcher checks it too: running java OrderService.java on a file that declares a package but sits outside a matching folder fails with "end of path to source file does not match its package name".
Packages are not nested
The dots look like a hierarchy, but to the compiler com.example.shop.orders and com.example.shop.orders.web are two unrelated packages. A package-private class in one is invisible to the other, and import com.example.shop.orders.*; brings in nothing from orders.web. The hierarchy exists only in folders and in tools that work "at or below" a starting package, such as Spring's component scanning.
The unnamed package
A file with no package line belongs to the unnamed package. That is fine for a script, and a compact source file (a bare void main() with no class around it) is always in it: adding a package line there is a compile error. Application code should not live there, because a class in the unnamed package cannot be imported by any class in a named package.
Package by feature, not by layer
Many services start with controller, service and repository packages. Every class then has to be public so the layers can reach each other, and one feature is spread across several folders. Grouping by feature (orders, customers, payments) keeps a feature's handler, service and repository together, lets the internals stay package-private, and keeps the public surface of each feature small. Spring Boot scans the package of the class annotated with @SpringBootApplication and everything below it, so that class belongs in the root package, for example com.example.shop. A class placed outside that tree is silently never picked up.
Example
// src/main/java/com/example/shop/orders/Order.java
package com.example.shop.orders;
import java.math.BigDecimal;
public record Order(long id, long customerId, BigDecimal amount) {}
// src/main/java/com/example/shop/orders/OrderRepository.java
package com.example.shop.orders;
import java.util.List;
// No modifier: package-private, an internal detail of the orders feature.
interface OrderRepository {
List<Order> findByCustomer(long customerId);
}
// src/main/java/com/example/shop/orders/OrderService.java
package com.example.shop.orders;
import java.math.BigDecimal;
// Public: the rest of the application talks to the feature through this class.
public class OrderService {
private final OrderRepository repository;
OrderService(OrderRepository repository) {
this.repository = repository;
}
public BigDecimal totalFor(long customerId) {
return repository.findByCustomer(customerId).stream()
.map(Order::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}When to use it
- Group an order service's handler, service and repository in one package so only the service is public.
- Place the Spring Boot application class in the root package so component scanning finds every feature below it.
- Set a log level for com.example.shop.payments alone, which works because logger names follow package names.
More examples
A package-by-feature layout
Each feature exposes a couple of public types and hides the rest. The test sits in the same package under src/test/java, which is how it reaches package-private classes without making them public.
src/main/java/com/example/shop/
ShopApplication.java package com.example.shop; (@SpringBootApplication)
orders/
Order.java public record
OrderController.java public, handles /orders
OrderService.java public, used by other features
OrderRepository.java package-private
JdbcOrderRepository.java package-private
customers/
Customer.java
CustomerService.java
CustomerRepository.java package-private
src/test/java/com/example/shop/orders/
OrderServiceTest.java same package, so it can see the package-private typesCompiling and running packaged code
You run a class by its full name, never by a path such as out/com/example/shop/App. The class path entry is the root folder (out), and the package name supplies the rest of the path.
# src/com/example/shop/App.java starts with: package com.example.shop;
# src/com/example/shop/orders/Order.java starts with: package com.example.shop.orders;
# -d writes class files into folders that mirror the package names
javac -d out $(find src -name '*.java')
ls out/com/example/shop
# App.class orders
# Run by fully qualified class name, with the root folder on the class path
java --class-path out com.example.shop.App
# The source launcher also works when the path matches the package;
# other source files in the same tree are compiled as they are needed
java src/com/example/shop/App.javaReading package and class names at run time
The full name is what logging configuration, Spring's component scan and serialisation libraries see, so it is worth knowing what it looks like. The empty package name confirms that a compact source file sits in the unnamed package.
void main() {
IO.println(String.class.getName());
IO.println(String.class.getPackageName());
// A nested type keeps its outer class in the binary name, joined with $
IO.println(Map.Entry.class.getName());
IO.println(Map.Entry.class.getPackageName());
IO.println(java.sql.Connection.class.getName());
// A compact source file always lives in the unnamed package
IO.println("[" + getClass().getPackageName() + "]");
}
// Output:
// java.lang.String
// java.lang
// java.util.Map$Entry
// java.util
// java.sql.Connection
// []
Discussion