Every software system reaches a point where the question is no longer how do we write this class? but where does this class belong? Directory structures grow organically, dependencies tangle across modules, and small changes ripple through packages that were never meant to know about each other.

This is the domain of package design, and it is governed by principles that have been well-understood for decades yet remain shockingly underapplied. Robert Martin distilled these into six principles: three concerning the cohesion of packages, three concerning their coupling.

Together they form a framework for reasoning about component boundaries. Master them, and you gain a vocabulary for the architectural decisions that determine whether your codebase remains a place developers want to work or becomes an artifact they endure. The rules are simple. Applying them well requires judgment.

The Cohesion Principles: What Belongs Together

The first question of package design is deceptively simple: which classes should live in the same component? Three principles answer this from complementary angles, and the tension between them is not a flaw—it is the design space itself.

The Reuse-Release Equivalence Principle states that the granule of reuse is the granule of release. If you want others to depend on your package, it must be versioned, released, and tracked as a coherent unit. Classes bundled together should share a lifecycle, not merely a directory.

The Common Closure Principle adds a maintenance lens: classes that change together belong together. When a business requirement shifts, the change should ideally touch one package, not scatter across five. This principle serves the developer modifying the code, minimizing the surface area of any single change.

The Common Reuse Principle approaches from the consumer's side: classes used together belong together, and—equally important—classes not used together should not be forced into the same package. Bundling unrelated utilities creates unnecessary dependencies for anyone who needs only a slice.

Takeaway

Cohesion is not about similarity of purpose—it is about shared destiny. Classes that release together, change together, and are used together belong together.

The Coupling Principles: How Components Relate

Once you have decided what belongs inside each package, the harder question emerges: how should packages relate to one another? Coupling principles govern the arrows on your dependency diagram, and getting them wrong produces systems that cannot be built, tested, or evolved independently.

The Acyclic Dependencies Principle is non-negotiable: the dependency graph between packages must be a directed acyclic graph. Cycles create components that can only be understood, built, and released as a single monolithic unit—defeating the entire purpose of packaging. When a cycle appears, break it with dependency inversion or extract a new component that both sides depend on.

The Stable Dependencies Principle requires that packages depend in the direction of stability. A volatile package that changes often should not be depended upon by a stable one, because every change forces revalidation downstream. Stability here is not about quality but about the difficulty of change—measured by how many other components rely on you.

The Stable Abstractions Principle completes the picture: a package's abstractness should match its stability. Stable packages should be abstract, so they can be extended without modification. Volatile packages should be concrete, free to change without disturbing anyone.

Takeaway

Dependencies should flow toward stability and abstraction. When they do not, every change becomes an archaeological expedition.

From Principle to Practice: Structuring Real Codebases

Principles become valuable only when they guide concrete decisions. Consider the moment you create a new class: where does it go? The cohesion principles suggest asking three questions in sequence. Will it release with these other classes? Will it change with them? Will consumers use it alongside them? Three yeses point to a home; hesitation suggests a new package.

For existing codebases, the coupling principles diagnose structural pain. Compute the instability metric—the ratio of outgoing dependencies to total dependencies—for each package. Then compute abstractness—the ratio of abstract types to total types. Plot them. Packages far from the diagonal between stable-abstract and unstable-concrete are candidates for restructuring.

Cycles announce themselves through build times and testing pain. When you cannot compile module A without also having B, and B cannot compile without A, you have a cycle regardless of what the diagram says. The fix is rarely to delete a dependency; it is to introduce an abstraction that both parties depend on, inverting the problematic arrow.

The most valuable habit is to treat package boundaries as design decisions, not accidents of directory creation. Every mkdir is an architectural choice. Every import across a boundary is a promise about coupling and stability that you and your successors will have to keep.

Takeaway

Package structure is a design artifact, not an emergent property. Treat every boundary as a deliberate commitment about what changes together and what depends on what.

The package principles are not academic curiosities. They are the accumulated wisdom of engineers who have watched codebases succeed and fail at scale, and they codify what those experiences taught.

You do not need to apply all six with mechanical precision. You do need to understand the trade-offs each one names, because when your architecture strains under growth, these are the levers you will reach for.

Design your packages as if the next developer to touch them has none of your context—because eventually, one of them will be you, and the kindness you extend now is the sanity you preserve later.