☕ Java
Package Protection
Package protection refers to the package-private access level — the default access when no modifier (public, protected, or private) is written. A package-private member is visible to all code within the same package but completely invisible to code outside the package, even if that code imports the package. Package protection is one of Java's four access levels and is a powerful but underused encapsulation tool that allows a group of collaborating classes to share implementation details without exposing those details to the broader codebase.
The Four Access Levels in Java
Java provides four access levels that control the visibility of classes, interfaces, constructors, methods, and fields. Each level defines a boundary beyond which the member is invisible. Understanding all four levels — and choosing deliberately among them rather than defaulting to public or private for everything — is a mark of thoughtful API design.
Private is the most restrictive: a private member is accessible only within the class that declares it. No other class can see it, not even a subclass. Private enforces the tightest possible encapsulation — implementation details that no code outside the class should ever touch are private.
Package-private (no modifier) is the second level: a member is accessible to all code in the same package. This includes other classes in the package, whether or not they are related by inheritance. Package-private is the default access — when you write a method or class without any access modifier, it is package-private. It is the correct level for implementation details shared between collaborating classes in the same package that should not be part of the public API.
Protected is the third level: a member is accessible within the same package (like package-private) and additionally from subclasses in any package. Protected is specifically designed for inheritance — it allows a superclass to expose members to subclasses without making them part of the public API. A subclass in a different package can access a protected member of its superclass through the inheritance relationship.
Public is the most permissive: a public member is accessible from any code anywhere, regardless of package. Public members form the API — the contract that the outside world depends on. Changes to public members break callers in all packages and potentially all users of a library.
The important principle is that access level should be as restrictive as possible while still satisfying the design's requirements. Start with private. If multiple classes in the same package need the member, use package-private. If subclasses in other packages need it, use protected. Only make it public when it genuinely needs to be part of the external API.
Java
// ── The four access levels: ───────────────────────────────────────────
public class AccessLevels {
public int publicField = 1; // visible everywhere
protected int protectedField = 2; // same package + subclasses
int packageField = 3; // same package only (no modifier)
private int privateField = 4; // this class only
public void publicMethod() { } // accessible everywhere
protected void protectedMethod() { } // package + subclasses
void packageMethod() { } // package only
private void privateMethod() { } // this class only
}
// ── Visibility from different locations: ──────────────────────────────
//
// Location public protected package private
// ─────────────────────────────── ────── ───────── ─────── ───────
// Same class ✓ ✓ ✓ ✓
// Same package (any class) ✓ ✓ ✓ ✗
// Different package (non-subclass) ✓ ✗ ✗ ✗
// Different package (subclass) ✓ ✓ ✗ ✗
// ── Package-private class — invisible outside its package: ────────────
package com.example.auth;
class TokenValidator { // no public — package-private class
boolean isValid(String token) {
return token != null && token.length() > 20;
}
}
public class AuthService { // public — visible outside the package
private final TokenValidator validator = new TokenValidator();
public boolean authenticate(String token) {
return validator.isValid(token); // uses package-private class
}
}
// From outside com.example.auth:
// import com.example.auth.TokenValidator; // COMPILE ERROR — package-private
// import com.example.auth.AuthService; // ✓ — public classPackage-Private as an Encapsulation Mechanism
The package is not just a namespace — it is a unit of encapsulation. A well-designed package exposes a minimal public API while keeping all implementation details hidden behind package-private access. The package boundary becomes as meaningful as the class boundary: just as a class's private members are its internal implementation, a package's package-private members are the package's internal implementation.
This design philosophy is particularly valuable when building subsystems. A payment processing subsystem might consist of a dozen internal classes — a charge calculator, a fraud detector, a gateway client, a retry handler, a response mapper — that collaborate extensively with each other. None of these internal classes need to be visible outside the subsystem. Only one or two public façade classes need to be exposed. The rest are package-private, forming the hidden implementation of the public API.
When deciding access level for a new member, the question to ask is: who legitimately needs to call this? If only the class itself, use private. If multiple classes in the same package need to coordinate through this member, use package-private. If subclasses in other packages are expected to use it as part of an extension mechanism, use protected. Only make it public when external callers — callers you have no control over — need to use it.
One of the most impactful refactoring steps when improving a messy codebase is reducing access levels. Finding every member that is public but never called from outside its package and changing it to package-private. Finding every member that is package-private but only called from within its own class and changing it to private. Each reduction in access level reduces the number of places a change can affect, making the code easier to reason about and modify.
Java
// ── Well-designed package with minimal public API: ───────────────────
// Package: com.example.payment
// PUBLIC — the only visible face of this package:
public class PaymentService {
private final ChargeCalculator calculator = new ChargeCalculator();
private final FraudDetector fraudCheck = new FraudDetector();
private final GatewayClient gateway = new GatewayClient();
public PaymentResult charge(String customerId, double amount,
String currency) {
double finalAmount = calculator.calculate(amount, currency);
if (fraudCheck.isSuspicious(customerId, finalAmount)) {
throw new FraudSuspectedException(customerId);
}
return gateway.send(customerId, finalAmount, currency);
}
}
// PACKAGE-PRIVATE — internal implementation, invisible outside the package:
class ChargeCalculator { // no modifier
double calculate(double amount, String currency) {
// conversion logic, tax, fees...
return amount;
}
}
class FraudDetector { // no modifier
boolean isSuspicious(String customerId, double amount) {
// ML model, velocity checks, threshold comparison...
return false;
}
}
class GatewayClient { // no modifier
PaymentResult send(String customerId, double amount, String currency) {
// HTTP call to payment gateway...
return new PaymentResult("TXN-001", "SUCCESS");
}
}
// ── From another package — only PaymentService is visible: ────────────
// com.example.order.OrderService:
import com.example.payment.PaymentService;
// import com.example.payment.GatewayClient; // COMPILE ERROR
// import com.example.payment.FraudDetector; // COMPILE ERROR
PaymentService payments = new PaymentService();
PaymentResult result = payments.charge("CUST-001", 99.99, "GBP");
// ── Progressive access reduction — a refactoring discipline: ──────────
// BEFORE: all public — everyone can depend on everything:
public class OrderProcessor {
public boolean validateStock(Order order) { ... }
public double applyDiscount(Order order) { ... }
public void updateInventory(Order order) { ... }
public void sendConfirmation(Order order) { ... }
public void process(Order order) { ... }
}
// AFTER: minimal public API — internals are package-private:
public class OrderProcessor {
public void process(Order order) { // only public method
validateStock(order);
double total = applyDiscount(order);
updateInventory(order);
sendConfirmation(order);
}
boolean validateStock(Order o) { ... } // package-private
double applyDiscount(Order o) { ... } // package-private
void updateInventory(Order o){ ... } // package-private
private void sendConfirmation(Order o){ ... } // private — no package need
}Package Protection and Testing
Package-private access creates a productive tension with testing. Unit tests should ideally test individual classes in isolation, including their package-private methods and classes. But test classes in a typical Maven or Gradle project live in src/test/java while production code lives in src/main/java. For test code to access package-private members, the test class must be in the same package as the class under test.
The solution is to place test classes in the same package as the production class they test, even though they are in different source root directories. A production class com.example.service.OrderService in src/main/java/com/example/service/ is tested by a test class also declared package com.example.service in src/test/java/com/example/service/. The test class is in the same package (same package name) and therefore has access to all package-private members, even though it is in a different physical directory.
This pattern is standard in professional Java development. It gives tests the access they need to verify internal behaviour without forcing you to make implementation details public. It also encourages keeping tests close to the code they test — the package structure of the test tree mirrors the structure of the production tree, making navigation between source and test straightforward.
The alternative of making members public for the sake of testability is a design anti-pattern. A method or class that is public exists in your API — callers depend on it and changes to it are breaking changes. Making something public solely for testing creates an API surface that you do not want to expose and that callers might accidentally depend on. Package-private with tests in the same package is always the better approach.
Java
// ── Production code: src/main/java/com/example/service/OrderService.java
package com.example.service;
public class OrderService {
// Package-private — internal implementation detail:
double calculateDiscount(double total, String customerTier) {
if ("PREMIUM".equals(customerTier)) return total * 0.15;
if ("STANDARD".equals(customerTier)) return total * 0.05;
return 0.0;
}
// Package-private validator:
boolean isValidOrder(Order order) {
return order != null
&& order.getItems() != null
&& !order.getItems().isEmpty()
&& order.getCustomerId() != null;
}
public OrderResult process(Order order) {
if (!isValidOrder(order)) {
throw new IllegalArgumentException("Invalid order");
}
double discount = calculateDiscount(order.getTotal(),
order.getCustomerTier());
// ...
return new OrderResult(order.getTotal() - discount);
}
}
// ── Test code: src/test/java/com/example/service/OrderServiceTest.java
package com.example.service; // SAME package — gets package-private access
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class OrderServiceTest {
private final OrderService service = new OrderService();
@Test
void calculateDiscount_premiumTier_returns15Percent() {
// Directly test package-private method — allowed because same package:
double discount = service.calculateDiscount(100.0, "PREMIUM");
assertEquals(15.0, discount);
}
@Test
void calculateDiscount_standardTier_returns5Percent() {
double discount = service.calculateDiscount(100.0, "STANDARD");
assertEquals(5.0, discount);
}
@Test
void isValidOrder_nullOrder_returnsFalse() {
// Test package-private method directly:
assertFalse(service.isValidOrder(null));
}
@Test
void process_invalidOrder_throwsException() {
// Test via public method:
assertThrows(IllegalArgumentException.class,
() -> service.process(null));
}
}
// ── Directory structure: ──────────────────────────────────────────────
// src/
// ├── main/java/com/example/service/
// │ └── OrderService.java (package com.example.service)
// └── test/java/com/example/service/
// └── OrderServiceTest.java (package com.example.service)
// ↑ same package declaration!Modules and Beyond Package Protection (Java 9+)
Package-private access provides encapsulation within a single package, but it has one significant limitation: all packages in the same compiled unit (classpath entry) can see each other's public members. If a library ships with both a public API package (com.example.api) and an intended-internal package (com.example.internal), nothing prevents a consumer from importing and using com.example.internal classes — they are public within that package. The convention "this is internal, do not use it" is not enforced by the compiler.
The Java Platform Module System (JPMS), introduced in Java 9, solves this. A module declares which packages it exports using the exports keyword in a module-info.java file. Packages not listed in exports are encapsulated at the module level — they are invisible to code in other modules even if their types are declared public. Public only means "public within this module" unless the package is exported.
This is a stronger form of encapsulation than package-private. With modules, even public classes in non-exported packages are inaccessible to consumers of the module. The JDK itself uses this extensively — many internal implementation packages (like sun.misc and com.sun.* that were previously accessible) are now encapsulated in modules and require explicit command-line flags to access.
For most application development, package-private access is sufficient. Modules add value for library authors who need to guarantee that consumers cannot depend on internal APIs, and for large multi-module projects where team boundaries map to module boundaries. Understanding that modules extend package protection to the module level completes the picture of Java's encapsulation mechanisms.
Java
// ── module-info.java — module descriptor (Java 9+): ──────────────────
// File location: src/main/java/module-info.java
module com.example.payment {
// These packages are exported — public types are accessible to others:
exports com.example.payment.api;
// These packages are NOT exported — completely hidden from other modules:
// com.example.payment.internal — inaccessible
// com.example.payment.gateway — inaccessible
// com.example.payment.fraud — inaccessible
// Export to a specific module only (qualified export):
exports com.example.payment.spi to com.example.payment.stripe,
com.example.payment.paypal;
// Declare dependencies on other modules:
requires java.net.http;
requires java.logging;
requires com.fasterxml.jackson.databind;
}
// ── Module encapsulation is STRONGER than package-private: ────────────
// package com.example.payment.internal:
public class InternalGatewayClient { // public within this module
public void send(String data) { }
}
// From another module (com.example.order) — CANNOT access:
// Even though InternalGatewayClient is public, the package is not exported!
// import com.example.payment.internal.InternalGatewayClient; // MODULE ERROR
// java: package com.example.payment.internal is not visible
// (package com.example.payment.internal is declared in module
// com.example.payment, which does not export it)
// ── Summary of encapsulation mechanisms: ─────────────────────────────
//
// Level Mechanism Enforced by
// ───────────────── ──────────────── ────────────────────
// Within a class private Compiler
// Within a package package-private Compiler
// Within a module unexported pkg Compiler + JVM
// Library internals Convention only Nothing (pre-modules)
// Library internals Module system Compiler + JVM (Java 9+)Related Topics in Packages and Access Control
Package Concept
A package in Java is a namespace that groups related classes, interfaces, enums, and annotations into a logical unit. Packages solve three fundamental problems in large software systems: name collision (two classes can have the same simple name if they are in different packages), access control (package-private visibility limits access to the same package), and organisation (related types live together and can be found intuitively). Every Java class belongs to a package — either explicitly declared or the unnamed default package.
Built-in Packages
Java ships with a rich standard library organised into packages under the java and javax namespaces. These built-in packages provide data structures, I/O, networking, concurrency, database access, XML processing, GUI components, and much more. Knowing which package contains which functionality, and understanding the most important classes in the most frequently used packages, is foundational knowledge for every Java developer.
User-defined Packages
User-defined packages are packages you create to organise your own application's classes and interfaces. They follow the same rules as Java's built-in packages — the package declaration must be the first statement in the file, the directory structure must match the package hierarchy, and access modifiers control visibility across package boundaries. Designing a meaningful package structure is a foundational software engineering skill that directly affects how maintainable and navigable a codebase remains as it grows.
import
The import statement allows you to use a class, interface, or enum by its simple name rather than its fully qualified name. Without an import, every reference to java.util.ArrayList requires writing the full name java.util.ArrayList. With import java.util.ArrayList, you write simply ArrayList. Imports are a compile-time convenience — they have no effect on performance, do not load classes, and are not present in the compiled bytecode. The compiler uses them only to resolve simple names to fully qualified names.