Organization

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/ or common/ 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.