Organização por Feature vs Camada
Como agrupar os arquivos de um projeto: por tipo técnico — todos os controllers juntos, todos os services juntos — ou por funcionalidade — tudo relativo a usuários numa pasta, tudo relativo a pedidos em outra. A escolha molda como o código cresce, como equipes se organizam e com que frequência você edita cinco diretórios para implementar uma única funcionalidade.
Intenção
Definir o critério primário de agrupamento de arquivos num projeto: as camadas técnicas que um arquivo habita (camada técnica) ou o domínio de negócio ao qual pertence (feature). A escolha determina onde um desenvolvedor procura e onde cria código novo — e, portanto, qual tipo de coesão o projeto maximiza.
Nenhuma das duas abordagens é universalmente correta. A tensão entre elas é real e a posição intermediária — feature com camadas internas — é a mais adotada em projetos de médio e grande porte.
Problema
Sem um critério claro de agrupamento, projetos crescem de forma caótica: arquivos relacionados ficam distantes, o desenvolvedor precisa navegar em múltiplos diretórios para entender uma única funcionalidade, e a arquitetura real do sistema se torna invisível na estrutura de pastas.
As duas abordagens resolvem esse problema de formas distintas — e cada uma cria seus próprios pontos de fricção à medida que o projeto cresce.
Estrutura
O diagrama abaixo mostra as mesmas funcionalidades — usuários, pedidos e pagamentos — organizadas das duas formas:
── POR CAMADA TÉCNICA ──────────────┬── POR 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 │
Para adicionar a funcionalidade de pagamento completa: na organização
por camada, você abre quatro diretórios diferentes. Na organização por
feature, você trabalha dentro de payments/.
Como funciona cada abordagem
Organização por camada técnica
O critério de agrupamento é o papel técnico do arquivo. Todos os controllers ficam juntos independente do domínio que servem; todos os services ficam juntos; todos os repositórios ficam juntos.
Isso maximiza coesão técnica: é fácil ver "todos os pontos de entrada HTTP" ou "todas as queries ao banco". É também o modelo que frameworks como Ruby on Rails e Laravel popularizaram — e onde a maioria dos desenvolvedores aprende arquitetura pela primeira vez.
Organização por feature
O critério de agrupamento é o domínio de negócio. Todos os arquivos
que servem a funcionalidade de "pedidos" ficam em orders/,
independente de seu papel técnico. Isso maximiza coesão funcional:
entender, modificar ou deletar uma funcionalidade é uma operação localizada
em um único diretório.
É o modelo natural quando o sistema cresce e times diferentes passam a
ser responsáveis por partes diferentes do domínio. O time de pagamentos
trabalha em payments/; o time de pedidos trabalha em
orders/. A estrutura de pastas reflete a estrutura de times.
Posição intermediária: feature com camadas internas
A abordagem mais adotada em projetos de médio e grande porte combina as duas: o nível superior organiza por feature, e dentro de cada feature as camadas técnicas aparecem como subdiretórios:
src/
├── users/
│ ├── http/ ← camada de entrada (controllers, DTOs)
│ │ ├── UserController.ts
│ │ └── CreateUserDto.ts
│ ├── application/ ← casos de uso / serviços de aplicação
│ │ └── UserService.ts
│ ├── domain/ ← entidades e regras de negócio
│ │ └── User.ts
│ └── infra/ ← repositório, ORM, adaptadores externos
│ └── UserRepository.ts
│
└── orders/
├── http/
├── application/
├── domain/
└── infra/
O resultado é que a feature é a unidade de descoberta (onde procurar) e a camada é a unidade de responsabilidade (o que cada arquivo pode fazer).
Quando usar cada abordagem
Prefira organização por camada quando:
- O projeto é um CRUD simples ou pequeno: poucas features, poucos arquivos por camada — a estrutura plana por camada é mais rápida de navegar do que subdiretórios por feature.
- A equipe é pequena e trabalha em tudo: sem divisão por times responsáveis por features, a separação por camada reflete melhor a forma de trabalho.
- O framework já impõe essa estrutura: frameworks como Rails (app/models, app/controllers, app/views) e Laravel (Controllers, Models, Requests) levam naturalmente à organização por camada. Lutar contra a convenção do framework costuma gerar mais fricção do que valor.
Prefira organização por feature quando:
- As funcionalidades crescem em número e complexidade: quando há 10+ features com 5+ arquivos cada, a estrutura por camada gera pastas com dezenas de arquivos sem relação entre si.
- Times diferentes são responsáveis por partes diferentes: a estrutura por feature alinha o código com a estrutura de ownership — reduz conflitos de merge e facilita code review focado.
- A arquitetura separa domínio de infraestrutura: quando o projeto usa Arquitetura em Camadas, Clean Architecture ou Hexagonal, a feature com camadas internas é a estrutura natural — cada feature é quase um módulo independente.
Prós e contras
Por camada — Prós
- Familiar para quem vem de frameworks como Rails, Laravel ou Spring MVC.
- Fácil de ver todos os pontos de entrada HTTP de uma vez, ou todos os repositórios.
- Estrutura plana e simples para projetos pequenos.
Por camada — Contras
- À medida que o projeto cresce, cada pasta fica com dezenas de arquivos sem relação entre si.
- Implementar uma feature exige abrir múltiplos diretórios simultaneamente.
- A estrutura não revela o que o sistema faz — revela apenas como é implementado.
- Dificulta deletar ou isolar uma feature — os arquivos estão espalhados por camadas.
Por feature — Prós
- Implementar uma feature é uma operação localizada num único diretório.
- A estrutura de pastas comunica o que o sistema faz, não apenas como.
- Facilita deletar ou mover uma feature para outro serviço.
- Alinha-se naturalmente com a estrutura de times e ownership.
Por feature — Contras
- Sem disciplina, features começam a importar umas das outras livremente — criando dependências circulares.
- Compartilhamento de código entre features (utilitários, tipos comuns) exige uma pasta
shared/oucommon/com critérios claros. - A visão "todos os controllers" ou "todos os services" requer navegar por múltiplas pastas.
Armadilhas comuns
1. Feature pura que vira silo
Sem boundaries explícitos, features em organização por feature começam a
importar umas das outras diretamente — o OrderService importa
o UserRepository, que por sua vez precisa do
PaymentService. O resultado é um emaranhado de dependências
cruzadas que impede qualquer uma das features de ser isolada ou testada
individualmente.
Regra prática: features só se comunicam por interfaces
públicas explícitas (DTOs, eventos, contratos de serviço), nunca por
imports diretos de arquivos internos de outra feature. Ferramentas de
lint como eslint-plugin-boundaries ou as restrições de módulo
do Nx podem enforçar isso automaticamente.
2. Camada pura que obriga editar 5 diretórios para 1 feature
A "camada técnica" começa simples, mas com 20+ features cada pasta fica
com 60+ arquivos. Adicionar "cadastro de fornecedor" significa criar um
arquivo em controllers/, um em services/, um em
repositories/ e um em entities/ — quatro commits
em quatro diretórios distintos para uma única funcionalidade coesa.
O sinal de que a estrutura por camada não escala mais: pull requests que tocam mais de três diretórios para uma única funcionalidade de negócio.
3. A pasta "shared/" sem critério
Ao adotar organização por feature, é inevitável que código genuinamente
compartilhado precise de um lar. A pasta shared/ (ou
common/) resolve isso — mas sem um critério de admissão claro,
ela se torna um segundo utils/ genérico onde tudo que não
tem um dono óbvio vai parar.
Critério útil: algo entra em shared/ apenas quando é usado
por duas ou mais features e não pertence semanticamente a nenhuma delas.
Se pertence semanticamente a uma feature mas é usada por outra, o design
de boundaries pode estar errado — talvez a dependência deva ser invertida.
4. Estrutura que não reflete a arquitetura
Uma estrutura por camada com pastas controllers/,
services/ e repositories/ pode dar uma falsa
sensação de arquitetura bem definida. Se o UserController
acessa o OrderRepository diretamente, a estrutura de pastas
é cosmética — não há uma Arquitetura
em Camadas real, apenas nomes de diretórios que imitam camadas.
Tópicos relacionados
A Arquitetura em Camadas define as regras de dependência entre partes do sistema; a organização por feature ou por camada define onde os arquivos ficam. As duas decisões se influenciam mas são independentes: você pode ter uma Arquitetura em Camadas com organização por feature (cada feature tem suas próprias camadas internas) ou com organização por camada técnica (camadas planas globais).
A decisão de monorepo vs multi-repo é outro eixo independente: ela define onde o código vive em termos de repositórios Git, enquanto feature vs camada define como ele é organizado dentro de cada projeto.