skip to content

Why does organising code into layer-based packages (controllers, services, repositories, models) usually violate the Common Closure Principle, and what packaging does CCP favour instead?

level: middleimportance: must knowfreq 50%

answer

  1. layers = orthogonal to the axis of change
  2. one feature change -> 4 packages edited
  3. feature on top, layers inside
  4. layer packages force everything public
  5. domain vs adapters split is a legitimate CCP-respecting layering

basics

~20 s

With layer packages, adding one feature field forces edits in every layer package at once. CCP wants classes that change together in one component, so it favours grouping by feature or business capability, with layers inside each feature.

solid answer

~50 s

Layer-based packaging groups classes by technical role, but requirements do not arrive along technical lines - they arrive as features. Adding a field to an order touches the controller, the service, the repository, and the model, so one change fans out across four components: four rebuilds, four reviews, four release notes, and possible lockstep versioning. That is exactly the shotgun surgery CCP exists to prevent. CCP favours package-by-feature (or package-by-component/capability): an 'ordering' component containing its own controller, service, repository, and model, so a change to ordering is confined to one deployable unit. Layers still exist, but as an internal arrangement inside a feature component rather than as the top-level partition. The bonus is encapsulation: a feature package can expose a narrow API and keep its internals package-private, which layer packages cannot do because every layer must be publicly visible to the layer above.

code

text · 15 lines
text
# package-by-layer: one 'add delivery slot to Order' change touches 4 packages
web/OrderController      <- edit
service/OrderService     <- edit
repo/OrderRepository     <- edit
model/Order              <- edit

# package-by-feature: same change touches 1 component
ordering/
  OrderController        <- edit
  OrderService           <- edit
  OrderRepository        <- edit
  Order                  <- edit
  OrderingApi            (only this stays public)
customers/  (untouched)
invoicing/  (untouched)

go deeper

for a junior

Show you understand the mechanics: name the four layer packages a single feature change would touch, and say CCP prefers grouping by feature.

for a middle

Add the cost model (rebuild, retest, redeploy, version bump), the encapsulation benefit of feature packages, and that layers still live inside a feature.

for a senior

Discuss the axis-of-change framing, acknowledge the cross-cutting technology counter-case and how an adapter component closes it, and connect to ownership/CODEOWNERS.

for a principal

Tie packaging to release and team topology: the top-level partition should match how the business issues change requests and how teams are formed, and should be re-cut when either shifts.

## The two packaging styles **Package-by-layer** (top-level split = technical role): ``` com.shop.web -> OrderController, CustomerController, InvoiceController com.shop.service -> OrderService, CustomerService, InvoiceService com.shop.repo -> OrderRepository, CustomerRepository, InvoiceRepository com.shop.model -> Order, Customer, Invoice ``` **Package-by-feature** (top-level split = business capability): ``` com.shop.ordering -> OrderController, OrderService, OrderRepository, Order com.shop.customers -> CustomerController, CustomerService, ... com.shop.invoicing -> InvoiceController, InvoiceService, ... ``` ## Why layering breaks CCP CCP asks: *when a requirement changes, how many components do I have to open?* Requirements are phrased by stakeholders as capabilities - "orders need a delivery-slot", "invoices need VAT per line". Such a change reaches down through every technical layer. Under package-by-layer that is one edit in each of four packages; if those packages are separately released artifacts, it is four version bumps released in lockstep. Under package-by-feature it is one package, one artifact, one release. Put formally: layering partitions along an axis (technical role) that is **orthogonal** to the axis along which change actually arrives (capability). CCP says partition along the axis of change. ## Secondary consequences of layer packaging - **No encapsulation.** Every class must be public because the layer above lives in a different package. Nothing can be hidden, so internal types leak into the whole codebase and become de facto public API. - **Merge contention.** Every team edits the same four packages, so hot spots and conflicts concentrate there. - **Deleting a feature is a hunt.** Removing 'invoicing' means finding its fragments in four places; under package-by-feature you delete a directory. - **Ownership is impossible to express.** CODEOWNERS or team boundaries cannot map onto a package that contains slices of every feature. ## Where layering is still fine - **Inside a feature component**, layering is exactly the right internal structure - it preserves the dependency rule (web -> service -> persistence) and keeps domain logic free of I/O concerns. - When the top-level split is *architectural* rather than merely technical - e.g. separating a pure domain core from adapters in hexagonal/ports-and-adapters or Clean Architecture - the boundary tracks a genuine difference in *rate and reason* of change: the domain changes with business rules, adapters change with technology and external APIs. That satisfies CCP, because those really are different reasons and different times. - Small codebases: with 15 classes, either style is navigable; the cost of layer packaging grows with feature count. ## Nuance: cross-cutting change is real Some changes genuinely are technical and cut across features - "migrate from ORM A to ORM B", "switch the HTTP framework". Under package-by-feature these fan out across features, which is the mirror-image cost. Two answers: (a) such changes are far rarer than feature changes, so optimise for the common case; (b) isolate the technology behind a shared adapter/infrastructure component so the technology change is itself closed in one place. That is CCP applied to the *other* axis of change, deliberately. ## How to argue this in an interview Don't say "layers are bad". Say: **the top-level partition should follow the dominant axis of change; for most product code that is capability, not technical role, so features go on top and layers go inside.** Then mention the encapsulation and ownership bonuses, and acknowledge the cross-cutting-technology counter-case.

  • Doesn't package-by-feature just move the problem when a technology change hits every feature?
    Yes, symmetrically - but feature changes vastly outnumber framework migrations, so you optimise for the common case. You also mitigate it by hiding the technology behind a shared adapter/infrastructure component, so the technology change is itself closed in one place.
  • Is a hexagonal architecture's domain/adapters split a CCP violation, since it is a kind of layering?
    No. That boundary separates things that change for genuinely different reasons and at different times - business rules versus external technology - which is exactly what CCP's second clause asks for. The violating case is splitting one capability across role-named packages.
  • What extra benefit besides fewer touched components does feature packaging give?
    Real encapsulation. Layer packaging forces every class public so the next layer can see it; a feature package can expose one narrow API type and keep the rest package-private, which shrinks the effective public surface and prevents accidental coupling.

A hardware store arranged by material - all steel in aisle 1, all plastic in aisle 2 - versus arranged by job: the plumbing aisle, the electrical aisle. You arrive with a job to do, not with a material in mind, so the job-based layout means one trip down one aisle.

saying these in an interview costs you the question

  • Claiming layers are always wrong - layering is correct *inside* a feature component and for domain-vs-adapter separation
  • Assuming package-by-feature eliminates cross-cutting change rather than trading a common cost for a rarer one
  • Believing the choice is only about navigation/aesthetics, missing the rebuild-retest-redeploy cost that CCP is actually about
  • Splitting by feature but leaving every class public, throwing away the encapsulation benefit
  • Calling a shared 'model' or 'common' package CCP-compliant when it changes for every feature's reasons

context