How does package-by-feature versus package-by-layer affect coupling and cohesion at the package level, and are there situations where you'd deliberately choose package-by-layer despite its locality-of-change downsides?
answer
- cohesion: related things together
- coupling: dependency between modules
- feature packaging makes coupling honest
- small apps/libraries can still favor layers
basics
~20 sFeature packages keep related code together (high cohesion) and keep unrelated features apart (low coupling between packages). Layer packages do the opposite - unrelated features get lumped together, while each feature's own code is spread thin. Layer-based still makes sense for very small apps or when the tech layers themselves are the reusable, shared thing, like a generic data-access library.
solid answer
~50 sCohesion measures how strongly the contents of one module belong together; coupling measures how dependent modules are on each other. Package-by-layer produces low intra-package cohesion - `services/` bundles OrderService with UserService with PaymentService, which share nothing but being 'services' - while distributing high, cross-cutting coupling everywhere, since every controller must import a service and repository from other packages, for every single feature. Package-by-feature inverts this: each feature package is highly cohesive (its contents collaborate on one job) and inter-package coupling is minimized, ideally reduced to a small shared/common package or explicit published interfaces. I'd still choose package-by-layer for a genuinely small, single-purpose service (a handful of endpoints, one team, low expected feature growth) where the ceremony of feature boundaries adds navigation overhead without payoff, or for a library whose public surface really is organized by technical capability (e.g. a generic ORM or HTTP client) rather than by business feature, since there's no business domain to slice vertically.
go deeper
Should know the rough definitions of cohesion (things belong together) and coupling (dependency between things) even if shaky on package-level specifics.
Should be able to say which layout produces higher cohesion and lower inter-module coupling and roughly why.
Should give the mechanical explanation precisely (which imports cross which boundaries under each layout) and name at least one legitimate case for still using package-by-layer.
Should discuss this in terms of team topology and ownership boundaries (Conway's Law), and treat the choice as one lever in a broader modularization/ownership strategy, not a universal rule.
## Two metrics, pushed in opposite directions Cohesion and coupling are the two classic metrics used to judge whether a modularization is good, and they're the sharpest lens for comparing package-by-layer against package-by-feature, because the two layouts systematically push those metrics in opposite directions even though they contain exactly the same classes. ## Cohesion Cohesion asks: how related are the things grouped together in one module? **High cohesion** means everything in the module exists to serve one purpose and tends to change together. - Under package-by-layer, the `services/` package holds `OrderService`, `UserService`, `PaymentService`, `NotificationService` — classes that share a naming suffix and a rough architectural role (business logic), but have no functional relationship. `OrderService` never calls `UserService` for a shared purpose the package name implies; they're neighbors by accident of role, not by collaboration. That's **low cohesion**: you can't describe what `services/` is 'for', beyond 'business logic of some kind', and a change to one class in it tells you nothing about whether a neighboring class needs to change too. - Under package-by-feature, `orders/` holds `OrderController`, `OrderService`, `OrderRepository` — these classes exist purely to collaborate on one job, ordering, and typically do change together, exactly the definition of high cohesion. ## Coupling Coupling asks: how much does one module depend on another, and how many other modules does a given module's callers have to know about? - Under package-by-layer, `OrderController` in `controllers/` must import `OrderService` from `services/`, which must import `OrderRepository` from `repositories/`. That's true for every feature, so the dependency graph runs, cross-cutting, through every top-level package boundary for every single unit of business logic — coupling is **diffuse and structural**, baked into the layout itself, not something a team failed to prevent. - Under package-by-feature, that same call chain (`OrderController` -> `OrderService` -> `OrderRepository`) happens entirely within `orders/`, so it isn't inter-package coupling at all from the top-level perspective; the only coupling visible at the top level is if `orders/` needs something from `catalog/` (e.g. to check product availability), which is real, meaningful, business-level coupling worth seeing, rather than plumbing. ## The trade-off this exposes The trade-off this exposes: package-by-feature makes coupling **honest**. Coupling that remains visible between feature packages is coupling that reflects a real business relationship (orders depend on catalog data), which is useful information for a team deciding whether two features should be merged, split further, or given an explicit contract/interface between them. Package-by-layer, by contrast, hides that information inside a wall of technically-necessary-but-uninformative cross-layer imports, so you can't tell 'orders needs catalog' from 'orders needs its own repository' just by scanning import statements at the package level — everything looks like layer-crossing. ## When a senior engineer still picks layers Given that, why would a senior engineer ever still choose package-by-layer? A few legitimate cases. 1. **First, scale:** a small service with, say, three endpoints and one team maintaining it for its whole life doesn't have enough feature surface for vertical slicing to pay for itself — `orders/`, `users/`, `payments/` as separate packages inside a ten-class microservice is ceremony without payoff, and a flat or lightly layered structure is easier to scan in one glance. 2. **Second, genuinely cross-cutting technical concerns** that aren't a business feature at all — a generic caching layer, an HTTP client wrapper, a shared validation framework — are legitimately organized by technical role, because there's no business vertical to align them to; a library's own internal structure is naturally layer/role-organized because its public surface is a technical capability, not a business domain. 3. **Third, teams whose organizational structure and expertise are split by layer** (a dedicated persistence team, a frontend-facing API team) sometimes deliberately keep layer boundaries as ownership boundaries, accepting the locality-of-change cost as a trade for clean team-of-record ownership per layer — though this is increasingly rare as cross-functional 'you build it, you own it' teams become the norm, and Conway's Law arguments now more often favor feature-aligned teams and feature-aligned code together.
- Can package-by-feature still end up with high coupling between feature packages, and what does that usually signal?Yes - if `orders/` and `shipping/` constantly need to call deep into each other's internals, that's a strong signal the feature boundary is drawn wrong and the two should either be merged into one feature or given an explicit, narrow interface instead of reaching across freely. High inter-feature coupling under package-by-feature is a useful, visible warning sign that layer-based packaging would have hidden inside generic cross-layer imports.
- How does package-level cohesion under package-by-feature relate to encapsulation via visibility modifiers like package-private in Java or Kotlin?Because a feature's implementation classes (like OrderRepository) all live in the same package, they can be marked package-private so only OrderService, in the same package, can call them - external packages are physically prevented from bypassing the service layer. Under package-by-layer this doesn't work, because OrderRepository sits in a repositories package alongside every other feature's repository, so any package-private access grants access to unrelated features too, forcing everything to be public.
It's like the difference between a shared open-plan tool shed where every wrench for every project sits together sorted by size, versus a labeled toolbox per project - the toolbox groups tools that are actually used together (high cohesion), and if you need to borrow from another project's toolbox it's an obvious, visible event (real coupling), instead of everyone reaching into the same undifferentiated wrench bin.
saying these in an interview costs you the question
- Confuses cohesion and coupling or uses the terms interchangeably
- Claims package-by-layer never has a legitimate use case
- Doesn't recognize that inter-feature coupling under package-by-feature is itself useful signal, not just a downside
- Thinks the choice of packaging changes the actual runtime dependencies rather than how visible/organized they are