Introdução
Proposta por Alistair Cockburn, a Hexagonal Architecture (Ports & Adapters) promete algo que todo desenvolvedor deseja: código que sobrevive ao teste do tempo.
Neste artigo, vou mostrar como implementei essa abordagem no Sauron, um serviço de registro e avaliação de clientes, e como você pode aplicar os mesmos princípios nos seus projetos.
O Problema que Hexagonal Architecture Resolve
Quantas vezes você já viu (ou escreveu) código assim?
@Service
public class CustomerService {
@Autowired
private CustomerRepository repository; // JPA vazando por todo lado
public Customer createCustomer(CustomerDTO dto) {
Customer customer = new Customer();
customer.setName(dto.getName());
// ... mais 50 linhas misturando validação, negócio e persistência
}
}
Os problemas:
- Alto acoplamento com frameworks (Spring, JPA, etc.)
- Impossível testar sem subir o contexto Spring
- Mudança de banco de dados? Prepare-se para refatorar metade do sistema
- Regras de negócio perdidas em meio a anotações e configurações
Hexagonal Architecture resolve isso separando claramente as responsabilidades.
A Estrutura de Camadas
No projeto Sauron, a estrutura é dividida em três camadas principais:
src/main/java/com/github/thrsouza/sauron/
├── domain/ # Camada de Domínio (núcleo)
├── application/ # Camada de Aplicação (casos de uso)
└── infrastructure/ # Camada de Infraestrutura (adaptadores)
Apenas para esclarecer:
- Essa é a forma como eu gosto de organizar o meu código.
- Arquitetura de software não diz respeito a como diretórios ou pacotes são organizados ou nomeados.
1. Domain Layer: O Coração do Sistema
O domínio é o núcleo do seu sistema. Aqui vivem as regras de negócio puras, sem dependências externas.
package com.github.thrsouza.sauron.domain.customer;
public class Customer extends AggregateRoot {
private final UUID id;
private final String document;
private final String name;
private final String email;
private CustomerStatus status;
public static Customer create(String document, String name, String email) {
UUID id = UUID.randomUUID();
Customer customer = new Customer(id, document, name, email, CustomerStatus.PENDING);
customer.recordDomainEvent(new CustomerCreated(customer.id()));
return customer;
}
public void approve() {
if (this.status != CustomerStatus.PENDING) {
throw new IllegalArgumentException("Customer status is not pending");
}
this.status = CustomerStatus.APPROVED;
this.recordDomainEvent(new CustomerApproved(this.id()));
}
}
Características importantes:
- ✅ Zero anotações de frameworks (@Entity, @Service, etc.)
- ✅ Lógica de negócio clara e testável
- ✅ Invariantes protegidas (não pode aprovar cliente não-pendente)
- ✅ Factory methods autodescritivos (Customer.create())
2. Application Layer: Orquestrando o Fluxo
A camada de aplicação contém os casos de uso, que orquestram o domínio sem conter lógica de negócio.
package com.github.thrsouza.sauron.application.customer;
public class CreateCustomerUseCase {
private final CustomerRepository customerRepository;
private final DomainEventPublisher domainEventPublisher;
public CreateCustomerUseCase(CustomerRepository customerRepository,
DomainEventPublisher domainEventPublisher) {
this.customerRepository = customerRepository;
this.domainEventPublisher = domainEventPublisher;
}
public Output handle(Input input) {
// Verifica se já existe (idempotência)
Optional<Customer> existingCustomer =
customerRepository.findByDocument(input.document());
if (existingCustomer.isPresent()) {
return new Output(existingCustomer.get().id());
}
// Cria novo cliente
Customer customer = Customer.create(
input.document(),
input.name(),
input.email()
);
// Persiste e publica eventos
customerRepository.save(customer);
domainEventPublisher.publishAll(customer.pullDomainEvents());
return new Output(customer.id());
}
public record Input(String document, String name, String email) {}
public record Output(UUID id) {}
}
Repare que:
- ✅ É um POJO puro (Plain Old Java Object)
- ✅ Dependências explícitas via construtor
- ✅ Input/Output com Records (imutáveis)
- ✅ Orquestra domínio, repositório e eventos
3. Infrastructure Layer: Adaptadores para o Mundo Externo
A infraestrutura implementa as interfaces definidas no domínio.
Exemplo: Repository Adapter
@Component
public class CustomerRepositoryAdapter implements CustomerRepository {
private final CustomerJpaRepository jpaRepository;
private final CustomerJpaMapper mapper;
@Override
public Optional<Customer> findById(UUID id) {
return jpaRepository.findById(id)
.map(mapper::toDomain);
}
@Override
public void save(Customer customer) {
CustomerJpaEntity entity = mapper.toEntity(customer);
jpaRepository.save(entity);
}
}
O segredo: Inversão de Dependência
A interface CustomerRepository está no domínio, mas a implementação está na infraestrutura:
// Domain layer (define o contrato)
package com.github.thrsouza.sauron.domain.customer;
public interface CustomerRepository {
Optional<Customer> findById(UUID id);
void save(Customer customer);
}
// Infrastructure layer (implementa o contrato)
package com.github.thrsouza.sauron.infrastructure.persistence.jpa.adapter;
@Component
public class CustomerRepositoryAdapter implements CustomerRepository {
// implementação
}
Isso é Dependency Inversion Principle em ação: módulos de alto nível (domínio) não dependem de módulos de baixo nível (infraestrutura). Ambos dependem de abstrações.
Configuração Manual: Controle Total
No Sauron, os casos de uso são configurados manualmente via @Configuration:
@Configuration
public class UseCaseConfig {
private final DomainEventPublisher domainEventPublisher;
private final CustomerRepository customerRepository;
@Bean
public CreateCustomerUseCase createCustomerUseCase() {
return new CreateCustomerUseCase(customerRepository, domainEventPublisher);
}
}
Por que não usar @Service nos casos de uso?
- Mantém a camada de aplicação independente do Spring
- Facilita testes unitários (sem contexto Spring)
- Deixa as dependências explícitas
- Controle total sobre a criação dos objetos
Benefícios Reais
1. Testabilidade
Testar o domínio é trivial:
@Test
void shouldApproveCustomer() {
Customer customer = Customer.create("12345678900", "John", "john@example.com");
customer.approve();
assertEquals(CustomerStatus.APPROVED, customer.status());
}
Sem Spring, sem banco, sem Kafka. Testes rápidos e confiáveis.
2. Substituição de Frameworks
Quer trocar JPA por MongoDB? Apenas crie um novo adapter:
@Component
public class CustomerMongoRepositoryAdapter implements CustomerRepository {
// Nova implementação
}
O domínio e a aplicação permanecem intocados.
3. Independência de Entrega
O mesmo domínio pode ser exposto via:
- REST API (Spring MVC)
- GraphQL
- gRPC
- CLI
- Mensageria (Kafka, RabbitMQ)
Basta criar adaptadores diferentes!
Armadilhas Comuns
❌ Domínio Anêmico
// ERRADO: Apenas getters e setters
public class Customer {
private CustomerStatus status;
public void setStatus(CustomerStatus status) {
this.status = status; // Sem validação!
}
}
✅ Domínio Rico
// CORRETO: Comportamento e validação
public class Customer {
public void approve() {
if (this.status != CustomerStatus.PENDING) {
throw new IllegalArgumentException("Customer status is not pending");
}
this.status = CustomerStatus.APPROVED;
}
}
❌ Vazamento de Infraestrutura
// ERRADO: JPA vazando para o domínio
public interface CustomerRepository {
Page<Customer> findAll(Pageable pageable); // Pageable é do Spring Data!
}
✅ Abstrações Puras
// CORRETO: Abstrações do domínio
public interface CustomerRepository {
List<Customer> findAll(int page, int size);
}
Quando NÃO usar Hexagonal Architecture
Hexagonal Architecture não é bala de prata. Evite em:
- Protótipos ou MVPs descartáveis
- CRUD simples sem lógica de negócio
- Scripts ou ferramentas internas
- Projetos com deadline apertado e time inexperiente
Para esses casos, um MVC tradicional pode ser mais adequado.
Conclusão
Hexagonal Architecture não é sobre estrutura de pastas ou quantidade de camadas. É sobre separação de responsabilidades, testabilidade e independência de frameworks.
Lembre-se: Arquitetura é sobre trade-offs. Hexagonal Architecture adiciona complexidade inicial em troca de sustentabilidade a longo prazo. Escolha sabiamente!
Top comments (2)
Hexagonal faz mais sentido quando você começa a sofrer com aquele acoplamento pesado do Spring espalhado pelo código. A ideia de deixar o domínio “limpo” e jogar toda a bagunça de frameworks pros adapters realmente muda o jogo — os testes ficam leves e trocar infraestrutura vira quase plug-and-play
Exatamente! Fácil de testar e, quando o domínio está livre do peso do framework, mudar a infraestrutura deixa de ser uma dor de cabeça. 😉