A maioria dos engenheiros já viu a imagem da clean architecture: quatro anéis coloridos com setas apontando para o centro. Bem menos gente sabe dizer o que vai em cada anel para uma funcionalidade real, para onde os imports devem apontar ou onde fica o SQL.
A lacuna aparece no code review: uma pasta domain que importa o ORM, um use case que devolve um código de status HTTP, cinco mappers entre um formulário e uma tabela.
Este post transforma os círculos de Robert C. Martin em um diagrama de componentes para um use case, fazer um pedido: as camadas, a regra de dependência, código e pastas, travessias de fronteira, clean versus hexagonal e onion, erros, quando é exagero e como desenhar.
As quatro camadas
Martin descreveu a clean architecture em um post de blog de 2012, The Clean Architecture, e a ampliou em seu livro de 2017, Clean Architecture. Ele a apresentou como uma integração de ideias anteriores, incluindo a arquitetura hexagonal e a onion. Do centro para fora:
| Camada | O que contém | No exemplo | Muda quando |
|---|---|---|---|
| Entities | Regras de negócio da empresa, reutilizáveis entre aplicações | Order e sua regra de que um pedido precisa de pelo menos uma linha |
Uma regra de negócio muda |
| Use cases | Regras de negócio da aplicação, um fluxo cada | PlaceOrder, suas duas interfaces, modelos de requisição e de resposta |
O fluxo muda |
| Interface adapters | Controllers, presenters, gateways e todo o SQL | OrderController, OrderPresenter, PostgresOrderRepository |
A API ou o esquema muda |
| Frameworks and drivers | Ferramentas mais código de ligação: framework web, banco de dados, drivers | O framework HTTP, PostgreSQL, pg, main.ts |
Você troca uma ferramenta |
Os círculos são esquemáticos; Martin diz que você pode precisar de mais de quatro. A regra que os conecta nunca muda.
A regra de dependência
Nas palavras de Martin, «as dependências do código-fonte só podem apontar para dentro». O código de um círculo interno nunca menciona uma função, classe ou variável declarada em um círculo externo, nem usa seus formatos de dados. Para este exemplo:
entitiesnão importa nada das outras camadas.use-casesimporta apenasentities: sem framework web, ORM ou driver.- Os
adaptersimportam para dentro, mais a biblioteca que adaptam. - Só o
main.tsenxerga tudo.
A regra cobre o código-fonte, não as chamadas em tempo de execução: em execução, PlaceOrder chama para fora, o presenter e o banco de dados. Martin resolve isso com o princípio da inversão de dependência: o use case chama uma interface que pertence a ele, e uma classe externa a implementa. A chamada sai; a dependência aponta para dentro.
Uma dependência de fato aponta para fora: o adapter importa o driver do banco de dados. Isso é proposital. Martin coloca todo o SQL na camada de adapters, e nenhum código mais interno «deve saber absolutamente nada sobre o banco de dados».
Faça a regra valer no CI com a verificação onionArchitecture() do ArchUnit na JVM, com o dependency-cruiser para TypeScript ou com o contrato layers do import-linter para Python.
Exemplo completo: fazer um pedido
Uma requisição POST /orders segue este caminho:
- O framework web a roteia para
OrderController. - O controller converte o corpo em um
PlaceOrderRequest, um objeto simples que pertence à camada de use cases, cria umOrderPresentere chamaPlaceOrder.execute(). PlaceOrderdeixaOrder.place()aplicar as regras de negócio, salva por meio deOrderRepositorye passa umPlaceOrderResponseparaPlaceOrderOutput.OrderPresenterimplementa essa interface e monta um view model: status 201, um cabeçalhoLocatione um corpo JSON.- O controller devolve o view model, e o framework o escreve.
A camada de use cases, com as duas interfaces que pertencem a ela:
// src/orders/use-cases/place-order.ts
import { Order } from '../entities/order';
export interface OrderRepository { // implemented by an adapter
nextId(): Promise<string>;
save(order: Order): Promise<void>;
}
export interface PlaceOrderOutput { // implemented by the presenter
present(response: PlaceOrderResponse): void;
}
export type PlaceOrderRequest = {
customerId: string;
lines: { sku: string; quantity: number }[];
};
export type PlaceOrderResponse = { orderId: string; itemCount: number };
export class PlaceOrder {
constructor(private readonly orders: OrderRepository) {}
async execute(request: PlaceOrderRequest, output: PlaceOrderOutput): Promise<void> {
const order = Order.place(await this.orders.nextId(), request.customerId, request.lines);
await this.orders.save(order);
output.present({ orderId: order.id, itemCount: order.itemCount() });
}
}
Ela importa apenas a entity. Order.place() lança um erro quando um pedido não tem linhas ou tem quantidade abaixo de um, e o arquivo da entity não importa nada. O adapter importa para dentro, mais o driver:
// src/orders/adapters/postgres/postgres-order-repository.ts
import { randomUUID } from 'node:crypto';
import type { Pool } from 'pg'; // adapters may import the driver
import type { Order } from '../../entities/order';
import type { OrderRepository } from '../../use-cases/place-order';
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly pool: Pool) {}
async nextId(): Promise<string> { return randomUUID(); }
async save(order: Order): Promise<void> {
await this.pool.query('INSERT INTO orders (id, customer_id, lines) VALUES ($1, $2, $3)',
[order.id, order.customerId, JSON.stringify(order.lines)]);
}
}
Só o composition root conhece cada classe concreta:
// src/main.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const controller = new OrderController(new PlaceOrder(new PostgresOrderRepository(pool)));
app.post('/orders', async (req, res) => {
const view = await controller.post(req.body); // the presenter's view model
res.status(view.status).set(view.headers).json(view.body);
});
Um teste unitário de PlaceOrder precisa apenas de um repositório em memória e de um presenter que registre sua entrada: sem servidor, sem banco de dados.
Estrutura de pastas
src/
orders/
entities/
order.ts Order and its rules
use-cases/
place-order.ts PlaceOrder, both interfaces, request and response models
adapters/
http/
order-controller.ts
order-presenter.ts
postgres/
postgres-order-repository.ts
main.ts composition root: builds and wires everything
Agrupe por funcionalidade e depois por camada. O argumento da screaming architecture de Martin: as pastas de nível superior devem dizer o que o sistema faz (orders, payments), não qual framework ele usa. O capítulo de Simon Brown no mesmo livro separa organização de encapsulamento: pastas organizam o código, enquanto regras de acesso ou uma verificação de build o fazem valer.
Atravessando fronteiras
Cada travessia de fronteira carrega um tipo que pertence a uma camada:
| Travessia | O que trafega | Tipo pertence a | Mecanismo |
|---|---|---|---|
| Framework → controller | Requisição HTTP | Framework | Código de ligação em main.ts |
| Controller → use case | PlaceOrderRequest |
Use cases | Chamada direta |
| Use case → presenter | PlaceOrderResponse |
Use cases | Interface PlaceOrderOutput |
| Use case → banco de dados | Order |
Entities | Interface OrderRepository |
| Presenter → framework | View model | Adapters | Devolvido pelo controller |
Três hábitos mantêm essas travessias limpas:
- Modelos de requisição e resposta simples. Martin é direto: «Não queremos trapacear e passar entities ou linhas do banco de dados». O presenter recebe
PlaceOrderResponse, nuncaOrder, então renomear um campo da entity não mexe na API. Repositórios são uma exceção comum: muitas bases de código deixam que aceitem entities, o que ainda aponta para dentro. Times mais rigorosos mapeiam para um registro de persistência dentro do adapter. - Uma interface em cada chamada para fora, pertencente à camada interna e nomeada pelo que o use case precisa, não pela tecnologia.
- Um único composition root. Mark Seemann define o composition root como «um local (de preferência) único em uma aplicação onde os módulos são compostos», perto do ponto de entrada. Martin chama
mainde «o detalhe supremo». Todo o resto recebe dependências por construtores.
A figura de Martin também tem uma interface Use Case Input Port na frente do interactor. Com um controller e uma implementação, chamar a classe diretamente é uma simplificação comum.
Clean versus hexagonal versus onion
| Hexagonal (ports and adapters) | Onion | Clean | |
|---|---|---|---|
| Texto de referência | Alistair Cockburn, artigo de 2005 | Jeffrey Palermo, série de blog de 2008 | Robert C. Martin, post de blog de 2012, livro de 2017 |
| Imagem | Aplicação em um hexágono, adapters fora | Anéis em volta do modelo de domínio | Quatro círculos concêntricos |
| Estrutura interna | Não prescrita | Modelo de domínio, serviços de domínio, serviços de aplicação | Entities, use cases |
| Implementação do repositório | Adapter secundário (dirigido) | Infraestrutura, anel externo | Interface adapters |
| Regra central | O código interno não deve vazar para fora; conversa por ports | Todo acoplamento aponta para o centro | As dependências do código-fonte apontam para dentro |
As três colocam as regras de negócio no meio e fazem as bordas dependerem delas. Diferem em quanta estrutura prescrevem por dentro: o artigo original de Cockburn separa apenas o interior do exterior, a onion architecture de Palermo nomeia anéis em volta do modelo de domínio, e a clean architecture acrescenta presenters e modelos de requisição e resposta. O diagrama de arquitetura hexagonal percorre um exemplo de pedido parecido em termos de ports and adapters.
Erros comuns
- Tipos de framework nas entities. Decorators de ORM ou atributos de serialização em
Orderprendem a camada mais interna à mais externa. Mapeie no adapter. - Um mapper para tudo. Seis cópias dos mesmos campos para um formulário que salva uma linha. Mapeie só onde as formas diferem.
- Camadas sem comportamento. Entities de getters e setters, o modelo de domínio anêmico que Martin Fowler descreveu em 2003, e use cases que só chamam
save(). - Interfaces em todo lugar. Elas pertencem às fronteiras, não a cada classe.
- HTTP no use case. Códigos de status pertencem ao presenter.
Quando é exagero
Palermo escreveu que a onion architecture «não é apropriada para sites pequenos», e o capítulo de Martin sobre fronteiras parciais abre com «Fronteiras arquiteturais completas são caras».
| Situação | Opção melhor |
|---|---|
| Serviço CRUD com poucas regras | Uma aplicação em camadas enxuta; mantenha as poucas regras fora dos controllers |
| Protótipo ou ferramenta de vida curta | Sem camadas |
| Uma dependência volátil, como um provedor de pagamento | Uma interface e um adapter para ela |
| Produto de vida longa com regras reais e vários pontos de entrada | As quatro camadas |
Esse capítulo também descreve fronteiras parciais mais baratas; promova uma quando uma segunda implementação ou um teste precisar dela.
O diagrama de clean architecture
Conteúdo do diagrama
- Frameworks and drivers
- Interface adapters
- Use cases
- Entities
- Web framework
- PostgreSQL
- OrderPresenter
- OrderController
- PostgresOrderRepository
- PlaceOrderOutput
- PlaceOrder
- OrderRepository
- Order
- POST /orders
- execute(request)
- Order.place()
- reads view model
- present(response)
- save(order)
- implements
- SQL via pg driver
Como ler:
- Quatro pacotes UML, o mais externo à esquerda. Ir para a direita é ir para dentro.
- Setas contínuas são chamadas e imports. Três atravessam uma fronteira em direção às entities. As outras três ficam dentro de uma camada:
present(response)esave(order)chamam as duas interfaces que pertencem à camada de use cases. - Setas tracejadas com triângulos vazados significam implementa. O presenter e o repositório apontam para dentro, para essas interfaces, enquanto o controle flui para fora.
- A linha pontilhada é SQL pelo driver, e ela começa em um adapter.
- As notas e o bloco Markdown dizem onde fica o
main.ts, o que a entity impõe e o que cada estilo de linha significa.
Desenhando diagramas de clean architecture com clareza
- Desenhe um use case, não os anéis. Trace uma funcionalidade real com nomes de classe reais.
- Coloque cada interface na camada a que ela pertence.
OrderRepositoryfica ao lado dePlaceOrder, não ao lado da sua implementação. - Use dois estilos de seta: contínua para chamadas, tracejada com triângulo vazado para implementa, como na realização do UML. O exemplo de diagrama de classes UML cobre a notação.
- Dê uma única direção a «para dentro» na página. Uma seta voltando é uma violação ou uma chamada em execução que precisa de rótulo.
- Rotule as setas com o método e o modelo que atravessa.
- Deixe de fora campos de DTO, internos de framework e o contêiner de DI.
Para o sistema ao redor desta visão no nível do código, veja o exemplo de diagrama do modelo C4 e o guia para desenhar diagramas de arquitetura de software.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Adicione quatro nós Pacote do pack UML em fila: Frameworks and drivers, Interface adapters, Use cases e Entities, com destaques azul, verde, vermelho e âmbar.
- Em Frameworks and drivers, adicione um Componente do pack UML chamado Web framework e um nó Postgres do pack Dados chamado PostgreSQL.
- Em Interface adapters, adicione dois nós Componente, OrderPresenter e OrderController, e uma Classe chamada PostgresOrderRepository.
- Em Use cases, adicione uma Classe chamada PlaceOrder entre dois nós Interface, PlaceOrderOutput e OrderRepository.
- Em Entities, adicione uma Classe chamada Order com seus campos e métodos.
- Desenhe seis arestas contínuas rotuladas para as chamadas, duas arestas tracejadas com pontas de triângulo para implementa e uma aresta pontilhada de PostgresOrderRepository para PostgreSQL.
- Adicione um bloco Markdown do pack Básico com a regra e os estilos de linha, e notas de Texto do pack Básico para cada camada.
Um fluxograma Mermaid das suas camadas vira um primeiro rascunho com a ferramenta de Mermaid para diagrama editável. O Free permite 15 nós de diagrama por diagrama, então trace um use case por diagrama.
O exemplo usa 14 nós de diagrama e fica dentro do plano Free.
Perguntas frequentes
Quais são as quatro camadas da clean architecture? Entities (regras de negócio da empresa), use cases (regras de negócio da aplicação), interface adapters (controllers, presenters e gateways) e frameworks and drivers (o framework web, o banco de dados e outras ferramentas). Martin diz que os círculos são esquemáticos: você pode precisar de mais de quatro.
O que é a regra de dependência? As dependências do código-fonte apontam apenas para dentro. O código de uma camada interna nunca nomeia nada declarado em uma camada externa, então os use cases não sabem nada sobre controllers, SQL ou o framework web.
Qual é a diferença entre clean architecture e arquitetura hexagonal? As duas mantêm as regras de negócio no meio e fazem os adapters dependerem delas. A arquitetura hexagonal separa apenas o interior do exterior, por meio de ports. A clean architecture também divide o interior em entities e use cases e nomeia presenters e modelos de requisição e resposta.
Onde fica a interface do repositório na clean architecture? Na camada de use cases, ao lado do use case que precisa dela. A implementação, com o SQL, pertence à camada de interface adapters, e o composition root a injeta na inicialização.
A clean architecture é exagero para projetos pequenos? Muitas vezes. Para serviços CRUD, protótipos e ferramentas de vida curta, as interfaces e os mapeamentos extras custam mais do que devolvem. Adicione interfaces para as dependências que você espera que mudem e faça as camadas crescerem quando surgirem regras de negócio reais.