Como aplicar o padrão hexagonal em projetos modernos
1. Fundamentos do Padrão Hexagonal (Ports and Adapters)
O padrão hexagonal, também conhecido como Ports and Adapters, foi introduzido por Alistair Cockburn em 2005 como uma resposta ao acoplamento excessivo entre lógica de negócio e infraestrutura. Sua motivação central é permitir que o domínio da aplicação seja testado, evoluído e mantido independentemente de tecnologias externas como bancos de dados, frameworks web ou serviços de terceiros.
A estrutura conceitual divide-se em três partes:
- Núcleo (Core): contém entidades, objetos de valor e regras de negócio, sem qualquer dependência externa.
- Portas (Ports): interfaces que definem contratos de entrada (inbound) e saída (outbound) do core.
- Adaptadores (Adapters): implementações concretas que conectam o core ao mundo externo (REST, banco de dados, filas, etc.).
Diferentemente da clean architecture (Robert C. Martin) e da onion architecture (Jeffrey Palermo), o padrão hexagonal enfatiza a simetria entre portas de entrada e saída, tratando todas as interfaces externas de forma equivalente. Enquanto a clean architecture usa círculos concêntricos, o hexagonal usa um hexágono visual para representar múltiplos pontos de conexão.
2. Projetando o Núcleo do Domínio (Core)
O core deve ser completamente independente de frameworks. Exemplo de entidade sem dependências externas:
public class Pedido {
private String id;
private List<ItemPedido> itens;
private StatusPedido status;
public Pedido(String id) {
this.id = id;
this.itens = new ArrayList<>();
this.status = StatusPedido.CRIADO;
}
public void adicionarItem(Produto produto, int quantidade) {
if (status != StatusPedido.CRIADO) {
throw new IllegalStateException("Pedido já finalizado");
}
itens.add(new ItemPedido(produto, quantidade));
}
public double calcularTotal() {
return itens.stream()
.mapToDouble(ItemPedido::getSubtotal)
.sum();
}
}
Portas de entrada (inbound ports) representam casos de uso:
public interface CriarPedidoUseCase {
Pedido executar(CriarPedidoCommand comando);
}
Portas de saída (outbound ports) definem contratos para infraestrutura:
public interface PedidoRepository {
void salvar(Pedido pedido);
Optional<Pedido> buscarPorId(String id);
List<Pedido> listarTodos();
}
3. Implementação de Adaptadores de Entrada (Primary Adapters)
Adaptadores REST traduzem requisições HTTP para chamadas ao core:
@RestController
@RequestMapping("/api/pedidos")
public class PedidoController {
private final CriarPedidoUseCase criarPedidoUseCase;
public PedidoController(CriarPedidoUseCase criarPedidoUseCase) {
this.criarPedidoUseCase = criarPedidoUseCase;
}
@PostMapping
public ResponseEntity<PedidoResponse> criar(@RequestBody CriarPedidoRequest request) {
CriarPedidoCommand comando = new CriarPedidoCommand(
request.getClienteId(),
request.getItens()
);
Pedido pedido = criarPedidoUseCase.executar(comando);
return ResponseEntity.ok(PedidoResponse.fromDomain(pedido));
}
}
Adaptadores CLI oferecem interface alternativa:
@Component
public class PedidoCliAdapter implements CommandLineRunner {
private final CriarPedidoUseCase useCase;
@Override
public void run(String... args) {
if (args.length > 0 && args[0].equals("criar-pedido")) {
CriarPedidoCommand comando = new CriarPedidoCommand(args[1], parseItens(args[2]));
Pedido pedido = useCase.executar(comando);
System.out.println("Pedido criado: " + pedido.getId());
}
}
}
Adaptadores para filas de mensagens (ex.: RabbitMQ):
@Component
public class PedidoMessageListener {
private final CriarPedidoUseCase useCase;
@RabbitListener(queues = "pedidos.criar")
public void processarMensagem(CriarPedidoMessage mensagem) {
CriarPedidoCommand comando = new CriarPedidoCommand(
mensagem.getClienteId(),
mensagem.getItens()
);
useCase.executar(comando);
}
}
DTOs e validação ficam exclusivamente na camada de adaptação, sem contaminar o core.
4. Implementação de Adaptadores de Saída (Secondary Adapters)
Adaptador de banco de dados SQL:
@Repository
public class PedidoRepositoryJpa implements PedidoRepository {
private final JdbcTemplate jdbcTemplate;
@Override
public void salvar(Pedido pedido) {
jdbcTemplate.update(
"INSERT INTO pedidos (id, cliente_id, status, total) VALUES (?, ?, ?, ?)",
pedido.getId(),
pedido.getClienteId(),
pedido.getStatus().name(),
pedido.calcularTotal()
);
}
}
Adaptador para API externa:
@Component
public class NotaFiscalHttpAdapter implements NotaFiscalGateway {
private final RestTemplate restTemplate;
@Override
public void emitir(Pedido pedido) {
NotaFiscalRequest request = new NotaFiscalRequest(pedido);
restTemplate.postForEntity("https://api.fiscal.gov/notas", request, Void.class);
}
}
Adaptador de cache:
@Component
public class PedidoCacheAdapter implements PedidoCachePort {
private final RedisTemplate<String, Pedido> redisTemplate;
@Override
public void armazenar(Pedido pedido) {
redisTemplate.opsForValue().set("pedido:" + pedido.getId(), pedido, 10, TimeUnit.MINUTES);
}
}
5. Inversão de Dependência e Injeção de Dependências
A regra fundamental: o core nunca importa classes de adaptadores. Todas as dependências apontam para dentro.
Configuração modular com contêiner DI (Spring Boot):
@Configuration
public class PedidoModuleConfig {
@Bean
public CriarPedidoUseCase criarPedidoUseCase(PedidoRepository repository) {
return new CriarPedidoUseCaseImpl(repository);
}
@Bean
public PedidoRepository pedidoRepository(DataSource dataSource) {
return new PedidoRepositoryJpa(dataSource);
}
}
A composição acontece no ponto de entrada da aplicação (classe main ou módulo de configuração), garantindo que o core permaneça puro.
6. Testabilidade e Isolamento com o Padrão Hexagonal
Testes unitários no core sem infraestrutura:
class CriarPedidoUseCaseTest {
@Test
void deveCriarPedidoComSucesso() {
PedidoRepository repository = mock(PedidoRepository.class);
CriarPedidoUseCase useCase = new CriarPedidoUseCaseImpl(repository);
CriarPedidoCommand comando = new CriarPedidoCommand("cli-1", List.of(
new ItemPedidoRequest("prod-1", 2)
));
Pedido pedido = useCase.executar(comando);
assertNotNull(pedido.getId());
assertEquals(StatusPedido.CRIADO, pedido.getStatus());
verify(repository).salvar(pedido);
}
}
Testes de contrato para adaptadores:
@SpringBootTest
class PedidoRepositoryContractTest {
@Autowired
private PedidoRepository repository;
@Test
void deveSalvarEBuscarPedido() {
Pedido pedido = new Pedido("ped-1");
repository.salvar(pedido);
Optional<Pedido> encontrado = repository.buscarPorId("ped-1");
assertTrue(encontrado.isPresent());
assertEquals("ped-1", encontrado.get().getId());
}
}
7. Adaptação para Projetos Modernos: Microsserviços e Cloud
Em microsserviços, cada serviço possui seu próprio core e adaptadores:
// Serviço de Pedidos
public class PedidoServiceCore {
private final PedidoRepository port;
private final EventPublisher eventPort;
}
// Serviço de Pagamentos
public class PagamentoServiceCore {
private final PagamentoRepository port;
private final NotificacaoGateway notificacaoPort;
}
Eventos assíncronos entre serviços via mensageria:
@Component
public class PedidoEventPublisher implements EventPublisher {
private final RabbitTemplate rabbitTemplate;
@Override
public void publicar(PedidoCriadoEvent evento) {
rabbitTemplate.convertAndSend("pedidos.exchange", "pedido.criado", evento);
}
}
Adaptadores cloud-native para funções serverless:
public class CriarPedidoFunction implements Function<CriarPedidoRequest, PedidoResponse> {
private final CriarPedidoUseCase useCase;
@Override
public PedidoResponse apply(CriarPedidoRequest request) {
CriarPedidoCommand comando = new CriarPedidoCommand(
request.getClienteId(),
request.getItens()
);
Pedido pedido = useCase.executar(comando);
return PedidoResponse.fromDomain(pedido);
}
}
8. Boas Práticas e Armadilhas Comuns
Evitar vazamento de infraestrutura para o core: nunca use annotations JPA, anotações de serialização ou dependências de frameworks no domínio.
// ERRADO: core contaminado com JPA
@Entity
public class Pedido {
@Id
private String id;
}
// CORRETO: core puro
public class Pedido {
private String id;
}
Gerenciamento de transações: o controle transacional deve ficar nos adaptadores de saída ou em um adaptador específico, nunca no core.
Quando simplificar: para CRUDs simples sem regras de negócio complexas, o padrão hexagonal pode ser excessivo. Aplique-o quando houver múltiplas fontes de entrada/saída ou regras de negócio que justifiquem o isolamento.
Referências
- Alistair Cockburn - Hexagonal Architecture — Artigo original que define o padrão Ports and Adapters, com motivação e exemplos conceituais.
- Martin Fowler - Patterns of Enterprise Application Architecture — Livro que estabelece fundamentos de arquitetura de software, incluindo repositórios e gateways usados no hexagonal.
- Spring Boot Documentation - Dependency Injection — Guia oficial sobre injeção de dependências e configuração modular com Spring.
- RabbitMQ Tutorial - Java — Tutorial prático para implementação de adaptadores de mensageria assíncrona.
- Clean Architecture by Robert C. Martin — Livro que explora princípios de desacoplamento e teste, complementares ao padrão hexagonal.
- Testing Hexagonal Architecture - Baeldung — Artigo técnico sobre estratégias de teste para aplicações hexagonais, com exemplos em Java.