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.

Syntaxpackage com.example.shop.orders; // first statement in the file import java.util.List; public class OrderService { } // full name: com.example.shop.orders.OrderService

A 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

Example · java
// 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.

Example · text
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 types

Compiling 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.

Example · bash
# 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.java

Reading 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.

Example · java
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

  • Be the first to comment on this lesson.