Avoid “packaging by feature,” since constructs at the same abstraction level typically need shared interfaces. In a banking application, for example, withdraw and deposit operations depend on similar underlying contracts. Separating them into distinct feature packages often duplicates interfaces and increases code entropy. Instead, organize packages to group functionality at a consistent abstraction level.
Each package should be self-contained and expose a simple, stable interface. This interface should be “deep” —encapsulating significant complexity behind a clean abstraction by hiding many implementation details. It should also avoid temporal coupling and minimize chattiness (excessive back-and-forth calls).
Ideally, an application forms a tree of self-contained packages with no cyclic dependencies, establishing a clear composition hierarchy. The only exceptions are cross-cutting concerns like logging or caching, which necessarily permeate multiple nodes since they cannot be fully encapsulated.
Beyond packaging by feature, another problematic pattern persists from the MVC era: organizing packages as taxonomies for technical concepts—controllers, services, views, and so on. This approach directly contradicts the principle of self-contained packages and should be avoided.
Think of yourself as a library maintainer. A package is essentially no different from a library in that a library can be integrated into projects whose existence you’re unaware of; similarly, the package you create can be consumed by higher levels of abstraction you haven’t anticipated. Design with this perspective in mind.