Visão geral
Abra o account-service do Hands-on 1 daqui a um ano. O time de produto quer um endpoint para o app mobile, o time de banco de dados quer migrar do Postgres para um serviço gerenciado, o time de segurança quer que as contas sejam criadas a partir de um evento Kafka, e alguém precisa escrever testes que rodem em segundos. Se as regras de negócio ("um e-mail só pode ser cadastrado uma vez", "uma senha tem pelo menos 8 caracteres") estiverem emaranhadas com anotações do Spring, SQL e códigos de status HTTP, cada um desses pedidos vira uma reescrita.
A arquitetura de software define como os componentes de um sistema são organizados, como se comunicam e — mais importante — quais componentes podem conhecer quais outros. As arquiteturas vistas aqui compartilham o mesmo objetivo fundamental: proteger a lógica de negócio de preocupações externas, como frameworks, bancos de dados e mecanismos de entrega. Essa separação importa porque as preocupações externas mudam com mais frequência do que as regras de negócio. Uma migração de banco de dados, uma atualização de framework ou um novo protocolo de API não deveriam exigir reescrever a lógica central.
Objetivos de aprendizagem
Ao final desta aula, você deve ser capaz de:
- Explicar cada princípio SOLID e identificar uma violação dele em código Java.
- Explicar a Regra de Dependência e posicionar uma classe de um serviço Spring Boot na camada certa da Clean Architecture.
- Projetar um serviço com portas e adaptadores, distinguindo portas de entrada (driving) de portas de saída (driven).
- Comparar as arquiteturas em camadas, Clean e Hexagonal, e justificar quando a estrutura extra compensa.
- Escrever um teste unitário para um caso de uso, substituindo o adaptador de banco de dados por um em memória.
- Descrever o CQRS e o seu custo (consistência eventual), e decidir quando ele vale a pena.
Slides
Clique nos slides e use ← → para navegar; F abre em tela cheia e P abre a visão do apresentador, com as notas. Abrir os slides em uma nova aba
Roteiro
Leia as páginas nesta ordem — cada uma se apoia na anterior:
-
1. Princípios SOLID
Cinco regras de projeto no nível das classes — Responsabilidade Única, Aberto/Fechado, Substituição de Liskov, Segregação de Interfaces, Inversão de Dependência — que tornam o código manutenível e testável. A base em que as duas arquiteturas abaixo se apoiam.
-
2. Clean Architecture
Organiza o código em camadas concêntricas com uma Regra de Dependência explícita: nada em uma camada interna pode referenciar algo de uma camada externa. Proposta por Robert C. Martin como uma síntese de ideias anteriores, incluindo a Hexagonal e a Onion.
-
3. Arquitetura Hexagonal
Coloca o núcleo da aplicação no centro e o expõe por meio de portas (interfaces). Os sistemas externos se conectam por adaptadores. Também chamada de Ports & Adapters, foi apresentada por Alistair Cockburn em 2005.
Esta página de visão geral termina com uma comparação entre as duas arquiteturas e com o CQRS, um padrão que separa os lados de escrita e de leitura de um serviço.
O problema central
Sem uma regra arquitetural explícita, as dependências se acumulam nas duas direções. Um UserService acaba importando anotações JPA; um controller contém lógica de validação; um método de repositório retorna códigos de status HTTP. O resultado é uma Big Ball of Mud3 (grande bola de lama) — um sistema em que cada componente conhece todos os outros, o que torna impossíveis os testes isolados, a substituição de partes e o trabalho em paralelo dos times.
As arquiteturas desta aula resolvem isso com uma única restrição: as dependências fluem em uma só direção, apontando para o núcleo estável.
flowchart LR
External["Mundo externo<br>(HTTP, BD, MQ, CLI)"]
Core["Núcleo da aplicação<br>(regras de negócio)"]
External -->|adaptadores / controllers| Core
Core -.->|nunca importa| External Uma dependência, aqui, significa uma referência em tempo de compilação: a classe A depende de B se o código-fonte de A menciona B (um import, o tipo de um campo, um new B()). Se B muda, A pode ter que mudar também — então as dependências devem apontar do que muda com frequência (frameworks, protocolos) para o que muda raramente (regras de negócio).
O ponto de partida: arquitetura em camadas
A maioria dos serviços Spring Boot — incluindo os do hands-on — começa com a clássica arquitetura em camadas (n-tier): controller → service → repository → banco de dados. É um bom primeiro passo, porque separa o tratamento do HTTP da lógica de negócio. Mas veja para onde apontam as setas:
flowchart LR
subgraph layered ["Em camadas: o negócio depende da persistência"]
direction LR
c1["AccountController"] --> s1["AccountService"]
s1 --> r1["JpaRepository<br>(Spring Data)"]
r1 --> db1[("Postgres")]
end
subgraph clean ["Clean / Hexagonal: a persistência depende do negócio"]
direction LR
c2["AccountController"] --> s2["AccountService"]
s2 --> p2["«interface»<br>AccountRepository"]
a2["AccountJpaAdapter"] -->|implementa| p2
a2 --> db2[("Postgres")]
end Na versão em camadas, o serviço — a lógica de negócio — depende da tecnologia de persistência. Na versão Clean/Hexagonal, o serviço é dono de uma interface expressa em termos de negócio, e o código de persistência depende dela. Essa única mudança é o Princípio da Inversão de Dependência (o "D" do SOLID) aplicado na fronteira, e é isso que as próximas três páginas desenvolvem.
Comparação lado a lado
| Aspecto | Em camadas (n-tier) | Clean Architecture | Arquitetura Hexagonal |
|---|---|---|---|
| Abstração central | camadas horizontais | camadas concêntricas + Regra de Dependência | hexágono + portas e adaptadores |
| Direção das dependências | de cima para baixo, rumo ao banco de dados | sempre para dentro | sempre rumo ao núcleo |
| Vocabulário | Controller · Service · Repository | Entities · Use Cases · Interface Adapters · Frameworks & Drivers | Domain · Application Services · Ports · Adapters |
| Estratégia de testes | em geral precisa de um banco de dados (ou de mocks de tipos do framework) | camadas internas testáveis isoladamente | núcleo testável substituindo os adaptadores |
| Flexibilidade | trocar o BD mexe nos serviços | framework e BD podem ser trocados | qualquer sistema externo pode ser trocado |
| Relação | o ponto de partida habitual | parte explicitamente da Hexagonal e da Onion, nomeando as camadas internas1 | a ideia original de fronteira2 |
Qual usar?
As duas arquiteturas são complementares e costumam ser usadas juntas. Pense na Hexagonal como a definição de como funciona a fronteira da aplicação (portas e adaptadores), e na Clean Architecture como a definição do que vai dentro dessas fronteiras (camadas com políticas de dependência explícitas). A maioria dos microsserviços modernos combina as duas.
Não exagere na arquitetura de um serviço CRUD
Portas, adaptadores e mappers têm um custo: mais classes, mais conversões, mais indireção para ler. Um serviço que só armazena e devolve registros, sem regras de negócio reais (uma tabela de consulta, um catálogo simples), pode continuar em camadas. A estrutura extra compensa quando as regras de negócio são ricas, o serviço vive por muito tempo ou você precisa trocar ou adicionar mecanismos de entrega e armazenamentos de dados. Os microsserviços ajudam aqui: cada serviço pode escolher o nível de estrutura de que precisa.
CQRS — Command Query Responsibility Segregation
Pense no catálogo de produtos da loja: milhares de requisições GET /products?category=... por segundo, alguns PUT /products/{id} por hora. Um único modelo precisa ser normalizado para escritas seguras e desnormalizado para buscas rápidas — e não atende bem a nenhum dos dois. O CQRS separa o lado de escrita (comandos que alteram o estado) do lado de leitura (consultas que retornam visões), permitindo que cada um seja modelado, armazenado e escalado de forma independente. A ideia estende a Command-Query Separation de Bertrand Meyer (um método ou altera o estado ou retorna dados, nunca os dois) de métodos para modelos inteiros; Greg Young deu o nome por volta de 20104.
flowchart LR
client(["Cliente"])
subgraph write ["Lado de escrita (Command)"]
cmd["Command Handler"]
store["Event Store<br>(append-only)"]
bus["Barramento de eventos<br>(Kafka / em processo)"]
end
subgraph read ["Lado de leitura (Query)"]
proj["Projetor<br>(constrói os modelos de leitura)"]
view[("Modelo de leitura<br>(otimizado para consultas)")]
end
client -->|"POST /orders (Command)"| cmd
cmd -->|"anexa OrderCreated"| store
store --> bus
bus -->|"projeta"| proj
proj --> view
client -->|"GET /orders (Query)"| view | Aspecto | Lado de comando | Lado de consulta |
|---|---|---|
| Responsabilidade | Valida e aplica as alterações | Serve visões desnormalizadas, otimizadas para consulta |
| Consistência | Forte (a escrita tem sucesso ou falha de forma atômica) | Eventual (o modelo de leitura fica atrás dos comandos) |
| Escalabilidade | Escala para a vazão de escrita | Escala de forma independente para a vazão de leitura |
| Armazenamento | Normalizado ou log de eventos | Tabelas ou documentos desnormalizados, com junções prontas |
O CQRS tem graus. A forma mais leve são dois conjuntos de classes sobre o mesmo banco de dados (os comandos passam pelo modelo de domínio; as consultas leem direto para DTOs). O passo seguinte é um armazenamento de leitura separado — por exemplo, Postgres para escritas e Redis ou um índice de busca para leituras — mantido em sincronia por eventos. O diagrama acima mostra a forma mais pesada, com um event store.
CQRS e Event Sourcing
O CQRS costuma ser combinado com Event Sourcing: em vez de atualizar uma linha no banco de dados, os comandos anexam eventos imutáveis a um event store. O modelo de leitura é reconstruído reproduzindo esses eventos por meio de um projetor. Essa combinação é poderosa, mas complexa — adote-a quando auditabilidade, consultas temporais ou uma assimetria muito grande entre leitura e escrita justificarem o custo. Os dois padrões são abordados na aula de Arquitetura orientada a eventos e implementados com Kafka no Hands-on 9.
Armadilhas do CQRS
- Ler as próprias escritas (read-your-own-writes): o usuário salva um produto e a lista ainda mostra o nome antigo, porque a projeção ainda não foi atualizada. A UI precisa tolerar ou esconder esse atraso.
- Aplicar em todo lugar: Martin Fowler alerta que o CQRS se encaixa apenas em algumas partes de um sistema e adiciona uma complexidade arriscada nas demais5. Use-o na parte quente e com muitas leituras (o catálogo), não em todos os serviços.
Pontos-chave
- Arquitetura trata de quem pode depender de quem: as dependências devem apontar dos detalhes voláteis (frameworks, bancos de dados, protocolos) para as regras de negócio estáveis.
- A clássica arquitetura em camadas faz a lógica de negócio depender da persistência; a Inversão de Dependência na fronteira inverte essa seta.
- O SOLID atua no nível das classes; a Clean (camadas + Regra de Dependência) e a Hexagonal (portas + adaptadores) aplicam a mesma ideia ao serviço inteiro, e normalmente são combinadas.
- O ganho é testabilidade e substituibilidade; o custo é mais classes e mapeamentos — ajuste a estrutura à complexidade do serviço.
- O CQRS separa escritas de leituras para que cada uma possa ser otimizada, ao preço da consistência eventual; ele não exige Event Sourcing.
-
MARTIN, R. C. Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall, 2017. ↩
-
COCKBURN, A. Hexagonal Architecture, 2005. ↩
-
FOOTE, B.; YODER, J. Big Ball of Mud. PLoP '97, 1997. ↩
-
YOUNG, G. CQRS Documents, 2010. ↩