Ir para o conteúdo principal
Desenvolvimento de Software14 min de leitura

Arquitetura hexagonal: como separar regras de negócio da tecnologia

Entenda como a arquitetura hexagonal separa regras de negócio de frameworks, bancos e APIs. Veja portas, adaptadores, um exemplo em Python e um roteiro para evoluir sistemas existentes.

M
Publicado em
Revisado em
Arquitetura hexagonal: como separar regras de negócio da tecnologia
Síntese Executiva & Principais Pontos

Entenda portas, adaptadores e inversão de dependência para desenvolver sistemas mais fáceis de testar, manter e evoluir.

Entenda portas, adaptadores e inversão de dependência para desenvolver sistemas mais fáceis de testar, manter e evoluir.

A arquitetura hexagonal organiza o software para que suas regras de negócio possam ser executadas e testadas sem depender diretamente de frameworks, bancos de dados ou APIs externas. Essas tecnologias continuam necessárias, mas se conectam à aplicação por fronteiras explícitas.

Imagine um sistema de orçamento: decidir o desconto permitido é uma regra comercial; receber uma requisição HTTP, executar SQL e enviar um e-mail são mecanismos tecnológicos. Quando tudo fica no mesmo lugar, uma mudança aparentemente pequena pode exigir conhecer o fluxo inteiro.

Neste artigo, vamos separar essas responsabilidades e acompanhar um exemplo em Python. O objetivo é ajudar a reconhecer onde portas e adaptadores trazem benefícios e onde uma estrutura mais simples já atende ao projeto.

O problema: quando a regra de negócio depende da tecnologia

Um controller recebe o pedido de orçamento, verifica o desconto, calcula o total, grava os dados e envia uma notificação. No início, concentrar essas tarefas pode parecer conveniente. O problema aparece quando a mesma regra precisa atender a outro canal ou ser alterada com segurança.

Se o limite de desconto está dentro do controller, uma rotina de importação pode acabar duplicando esse cálculo. Com o tempo, a API permite uma condição comercial e a importação aplica outra.

Os testes também ficam mais trabalhosos. Para verificar apenas se um desconto ultrapassa o limite, a equipe precisa iniciar a aplicação, preparar registros no banco e impedir o envio de e-mails reais.

Vale distinguir três responsabilidades:

  • Transporte: interpretar o JSON, verificar campos obrigatórios e produzir uma resposta HTTP.
  • Negócio: decidir quais descontos são permitidos e como calcular o valor do orçamento.
  • Persistência: converter os dados e executar a gravação conforme o banco utilizado.

Um JSON malformado é um problema de transporte. Um desconto comercialmente proibido continua inválido mesmo quando a solicitação chega por terminal, planilha ou tarefa agendada.

Essa mistura pode contribuir para a dívida técnica em sistemas, porque amplia o conhecimento necessário para cada alteração. Ainda assim, dificuldades de manutenção também podem vir de requisitos pouco claros, testes insuficientes e decisões de modelagem. Adotar uma arquitetura, por si só, não resolve todos esses fatores.

O que é arquitetura hexagonal e o que o hexágono representa

A arquitetura hexagonal, também chamada de portas e adaptadores, estabelece uma fronteira entre o núcleo da aplicação e os mecanismos que interagem com ele.

O núcleo reúne os comportamentos de negócio e a coordenação dos processos que precisam ser protegidos de detalhes externos. As portas definem as interações permitidas nessa fronteira. Os adaptadores conectam essas interações a tecnologias concretas.

  • Núcleo: calcula o orçamento e aplica a política de desconto.
  • Porta: oferece a operação de criar orçamento ou define como solicitar sua persistência.
  • Adaptador: recebe HTTP, grava no PostgreSQL ou chama um serviço externo.

O hexágono é uma representação visual. Seus seis lados não exigem seis componentes, seis interfaces ou seis diretórios. O desenho ajuda a mostrar que a aplicação pode ter diferentes pontos de contato com o exterior.

Assim, o mesmo caso de uso pode ser acionado por uma API HTTP, um comando de terminal ou uma tarefa agendada. Cada entrada traduz sua própria linguagem para os dados esperados pela aplicação.

Esse modelo funciona em um monólito e não exige microsserviços. Também não depende de uma árvore específica de pastas: diretórios organizados ajudam a navegar, mas são as responsabilidades e as dependências do código que estabelecem as fronteiras.

Como separar domínio, casos de uso e infraestrutura

Uma divisão útil distingue domínio, aplicação e infraestrutura. Essa organização deve acompanhar a complexidade do problema, sem exigir uma classe para cada pequena operação.

Domínio: decisões que pertencem ao negócio

O domínio representa conceitos, cálculos e invariantes comerciais. No exemplo, ele determina que o valor-base deve ser positivo e que o desconto precisa respeitar a política definida.

Essas regras devem continuar válidas independentemente do canal de entrada. Por isso, verificar o limite apenas no formulário do navegador é insuficiente: outra integração pode enviar um desconto diferente.

Aplicação: coordenação do caso de uso

O caso de uso descreve uma ação oferecida pelo sistema. Criar um orçamento envolve receber os dados, aplicar as regras, construir o resultado e solicitar sua persistência.

Essa coordenação também pode envolver autorização e limites transacionais. Ela deve expressar o processo sem precisar interpretar uma requisição de um framework ou montar uma consulta SQL.

Infraestrutura: comunicação com recursos externos

Os adaptadores lidam com HTTP, ORM, banco de dados, arquivos e SDKs. Um adaptador de persistência transforma o orçamento no formato necessário para gravá-lo. O adaptador HTTP transforma a requisição em uma entrada do caso de uso e converte seu resultado em resposta.

Evite exigir que o núcleo receba objetos de requisição, sessões de ORM ou respostas de SDKs. Tipos simples e contratos próprios tornam mais clara a informação necessária para executar a operação.

A consistência precisa ser planejada junto dessa separação. Se criar um orçamento exige várias gravações atômicas, o caso de uso deve ter uma fronteira transacional adequada, expressa por um contrato quando necessário e implementada pela infraestrutura. Enviar e-mail depois da gravação exige outra decisão: uma falha no envio não desfaz automaticamente o que já foi salvo.

Portas de entrada, portas de saída e direção das dependências

As portas de entrada representam operações que a aplicação oferece. No exemplo, a operação é criar um orçamento com determinado valor-base e desconto.

As portas de saída representam capacidades externas de que a aplicação precisa. Persistir o orçamento é uma delas. O contrato deve descrever essa necessidade com a linguagem da aplicação, sem expor parâmetros de conexão ou detalhes de SQL.

Uma porta não precisa ser sempre uma interface formal. Conforme a linguagem e o problema, pode ser um protocolo, uma função ou um método público com tipos bem definidos.

Fluxo de execução e dependência de código são diferentes

Durante a execução, o caso de uso chama o repositório e a operação chega ao banco. Porém, o código do caso de uso importa apenas o contrato definido no núcleo. O adaptador PostgreSQL é que conhece esse contrato e oferece a implementação correspondente.

Esse é o papel do princípio da inversão de dependência: orientar as dependências para abstrações que protegem as políticas da aplicação.

A injeção de dependências é o mecanismo usado para fornecer a implementação. Em vez de criar um repositório PostgreSQL dentro do caso de uso, a aplicação o recebe por parâmetro. Não é necessário usar um contêiner ou uma biblioteca para fazer isso.

No ponto de inicialização, o programa monta as peças: cria o adaptador de persistência, fornece esse objeto ao caso de uso e conecta o caso de uso ao adaptador de entrada. Esse ponto pode conhecer as tecnologias concretas, pois sua responsabilidade é compor a aplicação.

Arquitetura hexagonal na prática: um fluxo de criação de orçamento

Vamos usar uma política ilustrativa: o desconto permitido varia de 0% a 10%, inclusive. Esse limite serve apenas ao exemplo; em um projeto real, deve refletir as regras acordadas com o negócio.

O código abaixo usa tipos da biblioteca padrão do Python. A função HTTP é ilustrativa: recebe um dicionário já interpretado e retorna status e corpo, sem depender de um framework.

from dataclasses import dataclass
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from typing import Protocol
from uuid import uuid4

# Domínio
class RegraDeNegocioInvalida(ValueError):
    pass

@dataclass(frozen=True)
class PoliticaDeDesconto:
    limite: Decimal = Decimal('10')

    def calcular_total(self, base: Decimal, desconto: Decimal) -> Decimal:
        if not base.is_finite() or base <= 0:
            raise RegraDeNegocioInvalida('O valor-base deve ser positivo.')
        if not desconto.is_finite() or not Decimal('0') <= desconto <= self.limite:
            raise RegraDeNegocioInvalida('Desconto fora do limite permitido.')
        total = base * (Decimal('1') - desconto / Decimal('100'))
        return total.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)

@dataclass(frozen=True)
class Orcamento:
    id: str
    valor_base: Decimal
    desconto_percentual: Decimal
    total: Decimal

# Porta de saída, definida conforme a necessidade da aplicação
class RepositorioDeOrcamentos(Protocol):
    def salvar(self, orcamento: Orcamento) -> None:
        ...

# Entrada independente de HTTP
@dataclass(frozen=True)
class CriarOrcamentoEntrada:
    valor_base: Decimal
    desconto_percentual: Decimal

# Caso de uso; executar é a operação de entrada oferecida
class CriarOrcamento:
    def __init__(self, repositorio: RepositorioDeOrcamentos,
                 politica: PoliticaDeDesconto):
        self.repositorio = repositorio
        self.politica = politica

    def executar(self, entrada: CriarOrcamentoEntrada) -> Orcamento:
        total = self.politica.calcular_total(
            entrada.valor_base, entrada.desconto_percentual
        )
        orcamento = Orcamento(
            id=str(uuid4()),
            valor_base=entrada.valor_base,
            desconto_percentual=entrada.desconto_percentual,
            total=total,
        )
        self.repositorio.salvar(orcamento)
        return orcamento

# Adaptador de saída para o exemplo e testes
class RepositorioEmMemoria:
    def __init__(self):
        self.itens: dict[str, Orcamento] = {}

    def salvar(self, orcamento: Orcamento) -> None:
        self.itens[orcamento.id] = orcamento

# Adaptador de entrada HTTP ilustrativo
# Contrato: valores monetários e percentuais chegam como strings.
def criar_orcamento_http(payload: dict, caso_de_uso: CriarOrcamento):
    try:
        base = payload['valor_base']
        desconto = payload['desconto_percentual']
        if not isinstance(base, str) or not isinstance(desconto, str):
            raise TypeError
        entrada = CriarOrcamentoEntrada(
            valor_base=Decimal(base),
            desconto_percentual=Decimal(desconto),
        )
    except (KeyError, TypeError, InvalidOperation, ValueError):
        return 400, {'erro': 'Informe os campos numéricos como texto válido.'}

    try:
        resultado = caso_de_uso.executar(entrada)
    except RegraDeNegocioInvalida as erro:
        return 422, {'erro': str(erro)}

    return 201, {'id': resultado.id, 'total': str(resultado.total)}

# Composição no ponto de inicialização
repositorio = RepositorioEmMemoria()
criar_orcamento = CriarOrcamento(repositorio, PoliticaDeDesconto())

status, corpo = criar_orcamento_http(
    {'valor_base': '1000.00', 'desconto_percentual': '15'},
    criar_orcamento,
)
# status: 422; nenhuma gravação foi solicitada.

A requisição é convertida em CriarOrcamentoEntrada. O caso de uso pede ao domínio que calcule o total. Como 15% ultrapassa o limite, a regra rejeita a operação antes da chamada a salvar. O adaptador de entrada traduz o erro para uma resposta HTTP.

Com 10% de desconto sobre 1.000,00, o cálculo retorna 900,00 e o orçamento é persistido. A escolha de Decimal e do arredondamento é explícita; moeda, precisão e limites de tamanho também precisam ser definidos no contrato de uma aplicação real.

Em um projeto maior, esses componentes podem ocupar módulos de domínio, aplicação e adaptadores. Essa distribuição deve preservar a direção das importações: o domínio não precisa importar o controller nem o repositório concreto.

Para usar PostgreSQL, seria necessário implementar outro adaptador com o método salvar e fornecê-lo na composição. A fronteira reduz o alcance dessa mudança, mas ainda exige implementar o mapeamento, configurar conexões e validar transações e falhas. Trocar de banco não se resume a substituir uma linha.

O exemplo omite autenticação, leitura do corpo HTTP, tratamento de falhas operacionais e persistência durável para manter o foco nas responsabilidades arquiteturais.

Como testar regras de negócio e adaptadores

A separação permite escolher o ambiente de teste conforme o comportamento verificado. Um cálculo de desconto não precisa de banco; uma consulta SQL precisa ser exercitada contra uma infraestrutura compatível.

Testes de domínio e de casos de uso

Os testes de domínio verificam cálculos e invariantes diretamente. Os testes de casos de uso verificam a coordenação, incluindo se a persistência acontece apenas depois da validação.

Considerando os componentes anteriores disponíveis no mesmo módulo, estas verificações ilustram o limite aceito e a rejeição sem gravação:

def test_aceita_desconto_no_limite():
    politica = PoliticaDeDesconto()
    total = politica.calcular_total(Decimal('1000.00'), Decimal('10'))
    assert total == Decimal('900.00')

def test_desconto_invalido_nao_persiste():
    repositorio = RepositorioEmMemoria()
    caso_de_uso = CriarOrcamento(repositorio, PoliticaDeDesconto())
    entrada = CriarOrcamentoEntrada(Decimal('1000.00'), Decimal('15'))

    try:
        caso_de_uso.executar(entrada)
    except RegraDeNegocioInvalida:
        pass
    else:
        raise AssertionError('O desconto deveria ter sido rejeitado.')

    assert repositorio.itens == {}

Também convém cobrir desconto zero, valores negativos, arredondamento e entradas não finitas. No adaptador HTTP, os testes devem verificar a tradução dos campos e das respostas.

Testes de integração e de contrato

O repositório em memória ajuda a testar a coordenação, mas não reproduz restrições, concorrência, precisão de colunas ou comportamento transacional de um banco real.

O adaptador PostgreSQL precisa de testes de integração que verifiquem gravação, leitura quando aplicável, mapeamento e rollback. Quando diferentes adaptadores prometem o mesmo comportamento, uma suíte de contrato pode verificar as expectativas compartilhadas.

Esses contratos precisam ser explícitos: por exemplo, o que significa salvar com sucesso e como conflitos são comunicados. A assinatura de um método, sozinha, não especifica toda a semântica.

Testes de ponta a ponta

Algumas jornadas essenciais devem atravessar o sistema completo. Criar um orçamento pela API e confirmar sua persistência verifica se composição, configuração e adaptadores funcionam juntos.

Testes de domínio, integração e ponta a ponta respondem a perguntas diferentes. O sucesso de um nível não substitui a cobertura dos demais.

Arquitetura hexagonal, Clean Architecture e arquitetura em camadas

Essas abordagens compartilham preocupações com organização e manutenção, mas destacam aspectos diferentes. Elas também podem ser combinadas em um mesmo sistema.

Comparação entre abordagens de arquitetura de software
AbordagemFocoFronteiras e dependências
Arquitetura hexagonalInterações entre o núcleo e o exterior.Portas definem contratos; adaptadores conectam tecnologias sem exigir que o núcleo importe suas implementações.
Clean ArchitectureSeparação entre políticas de negócio e detalhes, com entidades e casos de uso protegidos.Dependências de código apontam para as camadas internas; dados atravessam fronteiras em formatos apropriados.
Arquitetura em camadasDistribuição de responsabilidades, como apresentação, negócio e dados.A direção depende da implementação. Pode haver dependência direta da camada de dados ou contratos com inversão de dependência.

Considere dois projetos com pastas chamadas controllers, services e repositories. No primeiro, o serviço instancia um repositório SQL e recebe entidades do ORM. No segundo, recebe um contrato de persistência e trabalha com tipos próprios.

Os nomes das pastas são semelhantes, mas o segundo projeto estabelece uma fronteira diferente entre regras e tecnologia. É necessário observar as importações e os contratos para entender a arquitetura efetiva.

O Domain-Driven Design, ou DDD, complementa essa discussão ao orientar a modelagem do negócio e seus limites. Ele pode ser usado com portas e adaptadores, mas adotar arquitetura hexagonal não obriga a aplicar todos os padrões de DDD.

A escolha deve considerar o problema, o conhecimento da equipe e o custo das fronteiras propostas. Nenhum desses modelos garante qualidade apenas por estar representado em um diagrama.

Quando vale a pena adotar e quais erros evitar

A arquitetura hexagonal tende a ser útil quando existem regras comerciais relevantes, múltiplas integrações, diferentes canais de entrada ou dificuldade para testar comportamentos sem preparar muita infraestrutura.

Sistemas de orçamento, agendamento e pedidos podem se beneficiar quando suas políticas precisam evoluir sem se espalhar por controllers e integrações. Porém, o tipo de sistema é apenas uma pista: a complexidade real dos fluxos deve orientar a decisão.

Uma página institucional com um formulário simples pode funcionar bem com uma estrutura enxuta. Um CRUD básico também pode dispensar várias abstrações. O benefício precisa compensar o custo de navegar entre contratos, implementações e conversões.

Erros que enfraquecem a separação

  • Criar interfaces sem necessidade: abstrações devem representar fronteiras úteis, não acompanhar automaticamente cada classe.
  • Expor detalhes do ORM: exigir sessões, consultas ou entidades com carregamento implícito dentro do núcleo mantém o acoplamento.
  • Usar HTTP como linguagem do negócio: o domínio deve expressar falhas comerciais; o adaptador decide como apresentá-las no protocolo.
  • Antecipar extensões improváveis: preparar vários mecanismos de persistência sem uma necessidade concreta aumenta o trabalho atual.
  • Repetir mapeamentos sem benefício: cada transformação deve proteger uma fronteira ou adaptar uma representação necessária.

O desacoplamento de software também tem limites. Bancos e APIs diferem em consistência, consultas, latência e tratamento de falhas. Uma abstração pode reduzir o impacto dessas diferenças, mas não as elimina.

Antes de introduzir uma porta, vale formular uma pergunta concreta: qual decisão externa está dificultando compreender, testar ou alterar este comportamento? A resposta ajuda a desenhar um contrato proporcional ao problema.

Como começar em um sistema existente sem reescrever tudo

A adoção pode começar por um único fluxo. Escolha uma operação com regras claras e impacto controlável, como o cálculo de desconto de um orçamento.

  1. Registre o comportamento atual. Crie testes para condições relevantes, incluindo os erros e os contratos externos que precisam ser preservados.
  2. Localize as decisões comerciais. Identifique cálculos e condições misturados com leitura HTTP, consultas e notificações.
  3. Extraia uma política de negócio. Mova o cálculo para um componente independente e faça o controller atual utilizá-lo.
  4. Organize o caso de uso. Separe a coordenação do processo da tradução feita pelo controller.
  5. Introduza a porta necessária. Se a persistência impede testar ou evoluir o fluxo, defina o contrato e adapte a implementação existente.
  6. Valide o caminho completo. Verifique regras, integração, transações e respostas externas antes de ampliar a mudança.

Não é necessário alterar a API pública para melhorar as fronteiras internas. Quando preservar esse contrato for um requisito, os testes devem confirmar que campos, status e comportamentos relevantes continuam compatíveis.

Depois da extração, avalie resultados observáveis: a política está mais fácil de localizar? É possível testá-la sem iniciar serviços? Uma alteração comercial passou a exigir mudanças em menos pontos?

Se a resposta for positiva, aplique o aprendizado a outro fluxo. Se a estrutura adicionar mais navegação do que clareza, revise as abstrações antes de expandi-las.

A arquitetura hexagonal oferece um caminho para manter decisões comerciais compreensíveis enquanto a tecnologia evolui. Seu valor aparece nas fronteiras que ajudam a trabalhar com segurança, e não na quantidade de interfaces criadas.

Seu site ou sistema concentra regras comerciais em controllers e integrações difíceis de manter? Entre em contato pelo site da Codephix, em codephix.com, e apresente seu projeto para conversar sobre desenvolvimento e evolução de aplicações para sua empresa em Recife.

Perguntas e Respostas Rápidas

Resumo de dúvidas sobre este tema

Como funciona: O problema: quando a regra de negócio depende da tecnologia?

Um controller recebe o pedido de orçamento, verifica o desconto, calcula o total, grava os dados e envia uma notificação. No início, concentrar essas tarefas pode parecer conveniente. O problema aparece quando a mesma regra precisa atender a outro canal ou ser alterada com segurança.

O que é arquitetura hexagonal e o que o hexágono representa?

A arquitetura hexagonal, também chamada de portas e adaptadores , estabelece uma fronteira entre o núcleo da aplicação e os mecanismos que interagem com ele.

Como separar domínio, casos de uso e infraestrutura?

Uma divisão útil distingue domínio, aplicação e infraestrutura. Essa organização deve acompanhar a complexidade do problema, sem exigir uma classe para cada pequena operação.

Como funciona: Portas de entrada, portas de saída e direção das dependências?

As portas de entrada representam operações que a aplicação oferece. No exemplo, a operação é criar um orçamento com determinado valor-base e desconto.

C

Elaborado e revisado por

Max Alex · Equipe de Engenharia Codephix

Engenharia de Software

A Codephix é uma software house sediada em Recife (PE) com atuação em todo o Brasil, especializada no desenvolvimento de sistemas sob medida, arquitetura escalável, integrações críticas e inteligência artificial aplicada a negócios.

Pronto para conversar sobre o seu projeto?

A Codephix transforma desafios operacionais em sistemas que funcionam. Fale com a nossa equipe de engenharia.

WhatsApp