Estruturas de Projeto

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