skip to content

Under package-by-layer organization (separate `controllers/`, `services/`, `repositories/` packages each containing classes for every feature), how many files and packages typically need to change to add a new `discountCode` field to an `Order`, and how does that compare under package-by-feature organization (a single `orders/` package holding `OrderController`, `OrderService`, and `OrderRepository`)?

level: middleimportance: must knowfreq 80%

answer

  1. files scattered vs contained
  2. diff spans N unrelated folders
  3. merge conflicts across features
  4. blast radius should match change size

basics

~20 s

With layer folders, adding one field touches files spread across several separate top-level folders (controller, service, repo). With feature folders, all those files sit together in one orders folder, so the change stays in one place.

solid answer

~50 s

Adding `discountCode` to Order typically requires editing the DTO/request object, `OrderController`'s mapping, `OrderService`'s business logic, `OrderRepository`'s query/entity mapping, and possibly a validator - roughly four to six files. Under package-by-layer, those files live in `dtos`, `controllers`, `services`, `repositories`, and `validators` respectively: five separate top-level packages, interleaved with every other feature's files, so the diff jumps around the whole tree and a code reviewer has to open five unrelated-looking folders to see one coherent change. Under package-by-feature, the same five files all live inside `orders/`, so the diff is contained to one directory, the PR is easy to scope-review, and merge conflicts with other features (say, someone else touching `users/`) are structurally impossible since the folders never overlap. This is 'locality of change': the cost of a change should be proportional to its conceptual size, and feature packaging keeps that proportional while layer packaging inflates it with unrelated file-tree navigation.

go deeper

for a junior

Should be able to say, given a concrete small change, roughly which files need editing and notice they'd be in different folders under a layer layout.

for a middle

Should articulate the PR-review and merge-conflict consequences concretely, using a specific example like adding a field.

for a senior

Should be able to weigh locality-of-change gains against the risk of badly-drawn feature boundaries reproducing the same problem, and connect it to review/onboarding costs with real examples from experience.

for a principal

Should treat locality of change as one signal for deciding module/bounded-context boundaries org-wide, and discuss how to detect it happening at scale (e.g. conflict-hotspot analysis, PR file-span metrics) rather than only anecdotally.

## What locality of change means Locality of change is the principle that the physical distance between files that must change together — measured in how many folders you have to open and how far apart they are in the tree — should track the conceptual size of the change, not be inflated by how the codebase happens to be organized. It's one of the most concrete, day-to-day-felt consequences of the package-by-layer vs package-by-feature decision, because it shows up on every single feature PR, not just in architecture reviews. ## The mechanism, walked through concretely Walk through the mechanism concretely. Suppose a product team wants Order objects to support an optional discount code. In a typical layered web application this touches: 1. The request/response **DTO** that the HTTP layer serializes (add the field so clients can send/receive it). 2. The **controller's mapping logic** if it isn't purely automatic. 3. The **service class** that contains the business rule (e.g. 'discount codes older than 90 days are rejected'). 4. The **repository/entity** that persists the field to the database. 5. And often a **request validator** that checks the code's format. That's five classes for what the business considers one small feature. ## Under package-by-layer Under package-by-layer, those five classes live in five different top-level packages: - `dtos/OrderRequest.kt` - `controllers/OrderController.kt` - `services/OrderService.kt` - `repositories/OrderRepository.kt` - `validators/OrderValidator.kt` Each of those packages also contains the equivalent files for every other feature in the system — `UserRequest`, `PaymentRequest`, `ProductRequest` sit right next to `OrderRequest` in `dtos/`. The diff for this one small change therefore touches five folders, each otherwise full of unrelated code, and a reviewer opening the diff has to mentally reconstruct which five files, out of dozens of changed-looking-but-unrelated neighbors, belong together, by tracing class names, not folders. ## Under package-by-feature Under package-by-feature, all five classes live inside `orders/` (perhaps as `orders/OrderRequest.kt`, `orders/OrderController.kt`, etc., or one level deeper as `orders/dto`, `orders/service`). The diff is contained to one directory. Opening `orders/` in the IDE shows every file relevant to understanding the change; nothing from `users/` or `payments/` is mixed in. ## Why this matters beyond tidiness Why this matters in practice, beyond tidiness: - **Code review quality degrades with scattering.** A reviewer who has to jump between five distant folders is more likely to miss that the validator wasn't updated to match a new business rule, because the files aren't visually adjacent and don't show up together in a single directory listing. - **Onboarding also suffers.** A new engineer told 'go look at how orders work' under package-by-layer has to open five folders and manually filter by filename prefix; under package-by-feature they open one folder and see the whole feature. ## Merge conflicts Merge conflicts are a second, very mechanical consequence. - Under package-by-feature, two engineers working on `orders/` and `payments/` respectively touch entirely disjoint file sets almost by construction (they only collide if a shared/common package changes). - Under package-by-layer, every feature's files funnel through the same small set of top-level packages, so two engineers adding fields to two different entities can end up editing the same `services/` or `repositories/` package's import block or a shared barrel/index file, producing conflicts that have nothing to do with the actual business logic changing. ## The failure mode on the other side There is a real failure mode on the other side worth naming honestly: locality of change is not free if the feature boundary is drawn badly. If 'orders' and 'shipping' are so entangled that most changes to one require changes to the other, package-by-feature just relocates the scattering problem from 'across layer folders' to 'across two feature folders that should have been one feature', and you get the same blast-radius pain with extra indirection. This is why picking the feature boundaries themselves — typically aligned with bounded contexts or clear business capabilities — matters as much as the by-feature-vs-by-layer decision; a badly-sliced vertical cut is not automatically better than a well-understood horizontal one. ## What migrating teams report A real-world example: teams migrating a legacy Spring MVC monolith that started as `controller/service/repository/dto` packages toward a package-by-feature or modular-monolith layout almost universally report the same first symptom driving the migration — every sprint's PRs for unrelated features were producing merge conflicts in the same handful of top-level packages, and PR reviews were taking longer because reviewers couldn't tell, from the file tree, which changed files belonged to the same piece of work.

  • Does package-by-feature eliminate merge conflicts entirely?
    No - it eliminates conflicts caused purely by unrelated features sharing the same layer folder, but conflicts still happen within a feature (two people both editing OrderService) or in genuinely shared code like a common/shared package or a top-level dependency-injection wiring file. It shrinks the conflict surface, it doesn't remove it.
  • What's a concrete signal in day-to-day work that a package-by-layer codebase is suffering from poor locality of change?
    PR diffs routinely span four or more top-level packages for what the ticket describes as one small feature change, and code review comments frequently say things like 'did you update the validator too?' because the reviewer can't see all the relevant files in one place. Repeated merge conflicts in `services/` or `repositories/` between engineers working on unrelated tickets is another strong signal.

It's like keeping every knife in one kitchen drawer, every fork in another room, and every plate in the basement, sorted by utensil type, instead of keeping a full place-setting together in one drawer near the table - setting the table for one guest means walking to three rooms instead of opening one drawer.

saying these in an interview costs you the question

  • Thinks locality of change is purely a cosmetic/IDE-navigation convenience with no real cost
  • Can't name a concrete example of files that change together for one feature change
  • Assumes package-by-feature automatically prevents all merge conflicts
  • Doesn't recognize that badly-drawn feature boundaries can reproduce the same scattering problem

context