Introdução: por que sua aplicação não é seu banco de dados
\n\nImagine que você está construindo uma casa. Fundação, paredes, telhado — tudo planejado. Mas depois de um ano, você decide trocar a fiação elétrica. E aí descobre que os fios estão embutidos no concreto e as tomadas soldadas na armadura. Para trocar um único interruptor, é preciso demolir metade da casa.
\n\nMais ou menos assim é uma aplicação Java clássica, onde a lógica de negócios está fortemente entrelaçada com o framework, banco de dados ou controlador REST. Quer migrar de PostgreSQL para MongoDB? Reescrever os controladores de Spring MVC para JAX-RS? Bem-vindo ao inferno da refatoração.
\n\nA saída é a Arquitetura Hexagonal (também conhecida como "Portas e Adaptadores", ou "Clean Architecture" na interpretação de Alistair Cockburn). Não é uma bala de prata, mas é uma forma de tornar sua aplicação flexível, testável e independente do mundo externo.
\n\nHoje vamos analisar a Arquitetura Hexagonal em Java com exemplos reais de código, sem enrolação e sem discussões abstratas. Vamos nessa.
\n\nO que é Arquitetura Hexagonal (Portas e Adaptadores)?
\n\nA essência é simples: sua aplicação é um núcleo (core) que contém a lógica de negócios. Ao redor do núcleo, há um "hexágono" (na verdade, um número arbitrário de faces), através do qual o mundo externo se comunica com o núcleo.
\n\nCada face tem sua porta (interface). E o adaptador é a implementação da porta para uma tecnologia específica. Por exemplo:
\n- \n
- Porta "UserRepository" — adaptador "JpaUserRepository" ou "MongoUserRepository" \n
- Porta "SendNotification" — adaptador "EmailSender" ou "SmsSender" \n
A regra principal: o núcleo não sabe nada sobre bibliotecas externas, bancos de dados, frameworks web. Ele conhece apenas suas portas (interfaces). Isso traz várias vantagens:
\n- \n
- Independência da infraestrutura — você troca o BD ou framework sem mexer na lógica. \n
- Testabilidade — a lógica de negócios pode ser testada sem subir o contexto do Spring ou do banco. \n
- Legibilidade — o código do núcleo conta a história do domínio, não os detalhes técnicos. \n
Estrutura do projeto: como organizar o código
\n\nNão existe uma única estrutura de pacotes correta, mas proponho uma opção testada com base em DDD (Domain-Driven Design) e nos princípios Hexagonal:
\n\ncom.example.app\n├── domain\n│ ├── model // Entidades, Value Objects\n│ ├── port // Interfaces das portas (inbound/outbound)\n│ └── service // Serviços de negócio (casos de uso)\n├── adapter\n│ ├── inbound\n│ │ ├── rest // Controladores REST (adaptadores de entrada)\n│ │ └── messaging // Ouvintes Kafka/RabbitMQ\n│ └── outbound\n│ ├── persistence // Repositórios JPA, JDBC, MongoDB\n│ └── client // Clientes HTTP, APIs externas\n└── config // Configuração do Spring (se estiver usando)\n\n\nObserve: domain não importa nada de adapter. Isso é crítico. Se você vir @Entity ou @RestController no domain, significa que a arquitetura está quebrada.
Exemplo: sistema de gerenciamento de pedidos
\n\nVamos projetar um sistema simples: o usuário pode criar um pedido, e o sistema verifica a disponibilidade do produto no estoque e envia uma notificação.
\n\nPasso 1. Modelo de domínio (domain/model)
\n\n// Order.java\npublic class Order {\n private final OrderId id;\n private final List<OrderItem> items;\n private OrderStatus status;\n\n public Order(List<OrderItem> items) {\n this.id = new OrderId(UUID.randomUUID().toString());\n this.items = Collections.unmodifiableList(items);\n this.status = OrderStatus.PENDING;\n }\n\n public void confirm() {\n if (status != OrderStatus.PENDING) {\n throw new IllegalStateException("Only PENDING orders can be confirmed");\n }\n this.status = Ord