Module Boundaries and Visibility: What Should Be Public, What Should Not, and Enforcing the Difference with ArchUnit
Module Boundaries and Visibility
The logistics platform has 338 Java classes. Every single one is public. The public keyword appears on every class declaration not because every class needs to be visible to every other class, but because the IDE generates public class by default, and no developer has ever changed it.
When every class is public, there are no module boundaries. Any class can depend on any other class. The architecture diagram shows layers and modules, but the access modifiers permit a flat dependency graph where everything can reach everything.
Visibility modifiers are the only mechanism Java provides for enforcing boundaries at compile time. A package-private class can only be used within its own package. A class made package-private is a class removed from the system’s public API surface. Every class removed from the public surface reduces the number of possible dependencies, which reduces the number of things a reader might need to understand when working in any given package.
The logistics platform’s restructuring from Chapter 5 created feature-based packages. This chapter makes those packages into real modules by defining what is public, what is package-private, and enforcing the difference with ArchUnit.
This diagram shows a feature package with its public API surface and its internal implementation. Only two classes are public: the interface that other packages use, and the domain event that other packages consume. The remaining five classes are package-private. External packages can depend on the public surface but cannot reach the internals. The boundary is enforced by the compiler. When a developer tries to import a package-private class from another package, the code does not compile.