Feature-based vs Layer-based Organization
How to group a project's files: by technical type — all controllers together, all services together — or by functionality — everything related to users in one folder, everything related to orders in another. The choice shapes how the code grows, how teams organize themselves, and how often you edit five directories to implement a single feature.
Intent
Define the primary criterion for grouping files in a project: the technical layer a file belongs to (technical layer) or the business domain it belongs to (feature). The choice determines where a developer looks and where they create new code — and, therefore, which type of cohesion the project maximizes.
Neither approach is universally correct. The tension between them is real, and the middle-ground position — feature with internal layers — is the most widely adopted approach in medium and large projects.
Problem
Without a clear grouping criterion, projects grow chaotically: related files end up far apart, the developer has to navigate multiple directories to understand a single feature, and the system's actual architecture becomes invisible in the folder structure.
The two approaches solve this problem in different ways — and each creates its own friction points as the project grows.
Structure
The diagram below shows the same features — users, orders and payments — organized in both ways:
── BY TECHNICAL LAYER ──────────────┬── BY FEATURE ────────────────────────
│
src/ │ src/
├── controllers/ │ ├── users/
│ ├── UserController.ts │ │ ├── UserController.ts
│ ├── OrderController.ts │ │ ├── UserService.ts
│ └── PaymentController.ts │ │ ├── UserRepository.ts
│ │ │ └── User.entity.ts
├── services/ │ │
│ ├── UserService.ts │ ├── orders/
│ ├── OrderService.ts │ │ ├── OrderController.ts
│ └── PaymentService.ts │ │ ├── OrderService.ts
│ │ │ ├── OrderRepository.ts
├── repositories/ │ │ └── Order.entity.ts
│ ├── UserRepository.ts │ │
│ ├── OrderRepository.ts │ └── payments/
│ └── PaymentRepository.ts │ ├── PaymentController.ts
│ │ ├── PaymentService.ts
└── entities/ │ ├── PaymentRepository.ts
├── User.entity.ts │ └── Payment.entity.ts
├── Order.entity.ts │
└── Payment.entity.ts │
To add the complete payment feature: with layer-based
organization, you open four different directories. With
feature-based organization, you work inside payments/.
How each approach works
Organization by technical layer
The grouping criterion is the file's technical role. All controllers sit together regardless of the domain they serve; all services sit together; all repositories sit together.
This maximizes technical cohesion: it's easy to see "all the HTTP entry points" or "all the database queries." It's also the model that frameworks like Ruby on Rails and Laravel popularized — and where most developers first learn about architecture.
Organization by feature
The grouping criterion is the business domain. All files that
serve the "orders" feature sit in orders/,
regardless of their technical role. This maximizes
functional cohesion: understanding, modifying or
deleting a feature is a localized operation within a single
directory.
It's the natural model as the system grows and different teams
become responsible for different parts of the domain. The
payments team works in payments/; the orders team
works in orders/. The folder structure mirrors the
team structure.
Middle-ground position: feature with internal layers
The most widely adopted approach in medium and large projects combines both: the top level organizes by feature, and within each feature the technical layers appear as subdirectories:
src/
├── users/
│ ├── http/ ← entry layer (controllers, DTOs)
│ │ ├── UserController.ts
│ │ └── CreateUserDto.ts
│ ├── application/ ← use cases / application services
│ │ └── UserService.ts
│ ├── domain/ ← entities and business rules
│ │ └── User.ts
│ └── infra/ ← repository, ORM, external adapters
│ └── UserRepository.ts
│
└── orders/
├── http/
├── application/
├── domain/
└── infra/
The result is that the feature is the unit of discovery (where to look) and the layer is the unit of responsibility (what each file is allowed to do).
When to use each approach
Prefer layer-based organization when:
- The project is a small or simple CRUD: few features, few files per layer — a flat layer-based structure is faster to navigate than feature subdirectories.
- The team is small and works on everything: without a division of teams responsible for specific features, layer-based separation better reflects the way the team works.
- The framework already imposes this structure: frameworks like Rails (app/models, app/controllers, app/views) and Laravel (Controllers, Models, Requests) naturally lead to layer-based organization. Fighting the framework's convention usually creates more friction than value.
Prefer feature-based organization when:
- Features grow in number and complexity: when there are 10+ features with 5+ files each, layer-based structure produces folders with dozens of unrelated files.
- Different teams are responsible for different parts: feature-based structure aligns the code with the ownership structure — reduces merge conflicts and makes focused code review easier.
- The architecture separates domain from infrastructure: when the project uses Layered Architecture, Clean Architecture or Hexagonal, feature with internal layers is the natural structure — each feature is almost an independent module.
Pros and cons
By layer — Pros
- Familiar to anyone coming from frameworks like Rails, Laravel or Spring MVC.
- Easy to see all HTTP entry points at once, or all repositories.
- Flat, simple structure for small projects.
By layer — Cons
- As the project grows, each folder ends up with dozens of unrelated files.
- Implementing a feature requires opening multiple directories at once.
- The structure doesn't reveal what the system does — it only reveals how it's implemented.
- Makes it hard to delete or isolate a feature — the files are scattered across layers.
By feature — Pros
- Implementing a feature is a localized operation within a single directory.
- The folder structure communicates what the system does, not just how.
- Makes it easier to delete or move a feature to another service.
- Aligns naturally with team structure and ownership.
By feature — Cons
- Without discipline, features start freely importing from each other — creating circular dependencies.
- Sharing code across features (utilities, common types) requires a
shared/orcommon/folder with clear admission criteria. - Seeing "all the controllers" or "all the services" requires navigating multiple folders.
Common pitfalls
1. Pure feature-based organization turning into a silo
Without explicit boundaries, features in a feature-based
organization start importing from each other directly — the
OrderService imports the UserRepository,
which in turn needs the PaymentService. The result
is a tangle of cross-cutting dependencies that prevents any
feature from being isolated or tested individually.
Rule of thumb: features only communicate through
explicit public interfaces (DTOs, events, service contracts),
never through direct imports of another feature's internal
files. Lint tools like eslint-plugin-boundaries or
Nx's module restrictions can enforce this automatically.
2. Pure layer-based organization forcing 5 directories for 1 feature
"Technical layer" starts out simple, but with 20+ features each
folder ends up with 60+ files. Adding "supplier registration"
means creating a file in controllers/, one in
services/, one in repositories/ and one
in entities/ — four commits across four different
directories for a single cohesive feature.
The sign that layer-based structure no longer scales: pull requests that touch more than three directories for a single business feature.
3. The "shared/" folder without a criterion
When adopting feature-based organization, genuinely shared code
inevitably needs a home. The shared/ (or
common/) folder solves that — but without a clear
admission criterion, it becomes a second generic
utils/ where everything without an obvious owner
ends up.
Useful criterion: something goes into shared/ only
when it's used by two or more features and doesn't semantically
belong to any of them. If it semantically belongs to one feature
but is used by another, the boundary design might be wrong —
perhaps the dependency should be inverted instead.
4. Structure that doesn't reflect the architecture
A layer-based structure with controllers/,
services/ and repositories/ folders
can give a false sense of well-defined architecture. If
UserController accesses OrderRepository
directly, the folder structure is cosmetic — there's no real
Layered Architecture,
just directory names that mimic layers.
Related topics
Layered Architecture defines the dependency rules between parts of the system; feature-based vs layer-based organization defines where the files live. The two decisions influence each other but are independent: you can have a Layered Architecture with feature-based organization (each feature has its own internal layers) or with technical-layer organization (flat, global layers).
The monorepo vs multi-repo decision is another independent axis: it defines where the code lives in terms of Git repositories, while feature vs layer defines how it's organized within each project.