Clean Architecture: organizando seu projeto

1. Fundamentos da Clean Architecture

A Clean Architecture foi popularizada por Robert C. Martin (Uncle Bob) como resposta ao problema crônico do acoplamento excessivo e da rigidez do código em sistemas empresariais. Quando frameworks, bancos de dados e bibliotecas externas se infiltram em todas as camadas da aplicação, qualquer mudança se torna um pesadelo de manutenção.

O modelo propõe círculos concêntricos que representam diferentes níveis de abstração:

  • Entities (centro) — regras de negócio mais fundamentais
  • Use Cases — regras de negócio específicas da aplicação
  • Interface Adapters — conversão entre formatos externos e internos
  • Frameworks & Drivers (borda) — detalhes técnicos

A Regra da Dependência é o pilar central: dependências devem sempre apontar para dentro. Código interno nunca sabe de código externo. Isso permite que detalhes como banco de dados ou framework web sejam trocados sem impactar o núcleo do sistema.

2. Camada de Entidades (Enterprise Business Rules)

Entidades encapsulam as regras de negócio mais críticas e invariantes da organização. Elas não dependem de nada externo — nem de ORMs, nem de frameworks, nem de bibliotecas de serialização.

// Exemplo de entidade rica sem dependências externas
public class Cliente {
    private String id;
    private String nome;
    private String email;
    private boolean ativo;

    public Cliente(String id, String nome, String email) {
        if (nome == null || nome.trim().isEmpty()) {
            throw new IllegalArgumentException("Nome é obrigatório");
        }
        if (!email.contains("@")) {
            throw new IllegalArgumentException("Email inválido");
        }
        this.id = id;
        this.nome = nome;
        this.email = email;
        this.ativo = true;
    }

    public void desativar() {
        this.ativo = false;
    }

    public boolean isAtivo() {
        return ativo;
    }
}

Entidades anêmicas (apenas getters/setters) são um anti-padrão. Uma entidade deve conter comportamentos que garantam sua consistência interna.

3. Camada de Casos de Uso (Application Business Rules)

Casos de uso orquestram fluxos de negócio específicos. Eles definem portas (interfaces) para interagir com o mundo externo, mas nunca conhecem implementações concretas.

// Porta para repositório (definida no caso de uso)
public interface RepositorioPedidos {
    void salvar(Pedido pedido);
    Pedido buscarPorId(String id);
}

// Caso de uso concreto
public class CriarPedidoUseCase {
    private final RepositorioPedidos repositorio;
    private final ServicoNotificacao notificador;

    public CriarPedidoUseCase(RepositorioPedidos repositorio, 
                              ServicoNotificacao notificador) {
        this.repositorio = repositorio;
        this.notificador = notificador;
    }

    public OutputCriarPedido executar(InputCriarPedido input) {
        Cliente cliente = repositorio.buscarCliente(input.clienteId());
        if (cliente == null || !cliente.isAtivo()) {
            return OutputCriarPedido.erro("Cliente inválido");
        }

        Pedido pedido = new Pedido(cliente, input.itens());
        repositorio.salvar(pedido);
        notificador.enviarConfirmacao(cliente.getEmail(), pedido);

        return OutputCriarPedido.sucesso(pedido.getId());
    }
}

Objetos de entrada e saída (Input/Output) são DTOs simples que cruzam a fronteira entre casos de uso e adaptadores.

4. Camada de Adaptadores de Interface (Interface Adapters)

Esta camada faz a tradução entre o mundo externo (HTTP, banco, filas) e o mundo interno (casos de uso, entidades). Controladores, Presenters, DTOs e Mappers vivem aqui.

// Controller sem lógica de negócio
public class PedidoController {
    private final CriarPedidoUseCase casoDeUso;

    public PedidoController(CriarPedidoUseCase casoDeUso) {
        this.casoDeUso = casoDeUso;
    }

    public ResponseEntity<?> criarPedido(PedidoRequest request) {
        InputCriarPedido input = new InputCriarPedido(
            request.clienteId(), 
            request.itens()
        );
        OutputCriarPedido output = casoDeUso.executar(input);

        if (output.erro()) {
            return ResponseEntity.badRequest()
                .body(new ErroResponse(output.mensagem()));
        }
        return ResponseEntity.status(201)
            .body(new PedidoResponse(output.pedidoId()));
    }
}

Gateways são implementações concretas das portas definidas nos casos de uso. Por exemplo, um RepositorioPedidosJPA que implementa RepositorioPedidos.

5. Camada de Frameworks e Drivers (Infrastructure)

A camada mais externa contém implementações concretas: ORMs, frameworks web, clientes HTTP, sistemas de fila, cache. Aqui o código depende de bibliotecas externas sem problemas.

// Implementação concreta do repositório usando JPA
public class RepositorioPedidosJPA implements RepositorioPedidos {
    private final EntityManager em;

    public RepositorioPedidosJPA(EntityManager em) {
        this.em = em;
    }

    @Override
    public void salvar(Pedido pedido) {
        PedidoJPA entidade = PedidoMapper.paraJPA(pedido);
        em.persist(entidade);
    }

    @Override
    public Pedido buscarPorId(String id) {
        PedidoJPA entidade = em.find(PedidoJPA.class, id);
        return PedidoMapper.paraDominio(entidade);
    }
}

Note que a entidade JPA (PedidoJPA) é diferente da entidade de domínio (Pedido). O mapper faz a conversão entre elas, isolando o núcleo dos detalhes de persistência.

6. Organização de Diretórios e Módulos

Duas abordagens principais coexistem: organização por camada e por funcionalidade.

Por camada (estilo clássico):

src/
  entities/
    Cliente.java
    Pedido.java
  usecases/
    CriarPedidoUseCase.java
    interfaces/
      RepositorioPedidos.java
  adapters/
    controllers/
      PedidoController.java
    gateways/
      RepositorioPedidosJPA.java
    presenters/
      PedidoPresenter.java
  infrastructure/
    config/
      DatabaseConfig.java
    web/
      Rotas.java

Por funcionalidade (mais modular):

src/
  pedido/
    domain/
      Pedido.java
      RepositorioPedidos.java
    application/
      CriarPedidoUseCase.java
    infrastructure/
      PedidoController.java
      RepositorioPedidosJPA.java
  cliente/
    domain/
      Cliente.java
      RepositorioClientes.java
    application/
      CriarClienteUseCase.java
    infrastructure/
      ClienteController.java
      RepositorioClientesJPA.java

A segunda abordagem favorece a coesão e facilita a extração de microsserviços futuros.

7. Testabilidade e Manutenção

A Clean Architecture brilha nos testes. Como entidades e casos de uso não dependem de infraestrutura, podem ser testados isoladamente.

// Teste de caso de uso sem infraestrutura
public class CriarPedidoUseCaseTest {
    @Test
    void deveCriarPedidoComSucesso() {
        RepositorioPedidos repositorioMock = mock(RepositorioPedidos.class);
        ServicoNotificacao notificadorMock = mock(ServicoNotificacao.class);

        CriarPedidoUseCase casoDeUso = new CriarPedidoUseCase(
            repositorioMock, notificadorMock
        );

        InputCriarPedido input = new InputCriarPedido("cli-1", List.of("item-1"));
        OutputCriarPedido output = casoDeUso.executar(input);

        assertFalse(output.erro());
        verify(repositorioMock).salvar(any(Pedido.class));
    }
}

Benefícios a longo prazo: migrar de MySQL para PostgreSQL, trocar Express por Fastify ou substituir o serviço de email não quebra uma única linha do núcleo do sistema.

8. Armadilhas Comuns e Boas Práticas

Armadilha #1: Excesso de abstração. Nem todo projeto precisa de 5 camadas. Para CRUDs simples, a Clean Architecture pode ser overengineering. Adapte o modelo à complexidade real.

Armadilha #2: Vazamento de dependência. Bibliotecas de infraestrutura (anotações JPA, dependências HTTP) não devem aparecer em entidades ou casos de uso. Use mappers e adaptadores para isolar.

Armadilha #3: Entidades anêmicas. Colocar apenas getters/setters nas entidades e mover toda a lógica para casos de uso empobrece o modelo. Entidades devem proteger suas invariantes.

Dicas práticas para projetos pequenos/médios:
- Comece com apenas duas camadas: domínio (entidades + casos de uso) e infraestrutura
- Adicione adaptadores gradualmente conforme a complexidade cresce
- Use interfaces desde o início para portas, mesmo que haja apenas uma implementação
- Prefira injeção de dependência manual em vez de frameworks complexos em projetos pequenos

A Clean Architecture não é um dogma, mas um guia. O objetivo final é código que permaneça flexível, testável e compreensível ao longo dos anos — independentemente de quantas vezes os frameworks ao redor mudarem.


Referências