Arquitetura Hexagonal (Ports and Adapters), proposta por Alistair Cockburn, e um padrao arquitetural que isola a logica de negocio das dependencias externas (banco de dados, APIs, frameworks). O resultado: codigo mais testavel, manutenivel e flexivel. Em 2026, e adotada por times que buscam qualidade e longevidade do software.
O problema que a Arquitetura Hexagonal resolve
Na maioria dos projetos, o codigo de negocio esta misturado com detalhes de infraestrutura: controllers que tem logica de negocio, services que fazem queries SQL diretamente, validacoes de negocio dentro de middleware, entidades ORM que sao tambem modelos de dominio.
O problema: para testar a logica de negocio, voce precisa de um banco de dados rodando. Para trocar de banco (PostgreSQL para MongoDB), precisa reescrever a logica. Para trocar de framework (Express para Fastify), toca em tudo. O acoplamento e total.
Como funciona: Ports and Adapters
A ideia central: a logica de negocio (dominio) fica no centro, totalmente isolada. Ela se comunica com o mundo externo atraves de Ports (interfaces/contratos) e Adapters (implementacoes concretas).
Port: interface que define O QUE o dominio precisa, sem dizer COMO. Exemplo: UsuarioRepository com metodo buscar_por_id(id) — nao diz se e PostgreSQL, MongoDB ou arquivo.
Adapter: implementacao concreta do Port. PostgreSQLUsuarioRepository implementa UsuarioRepository usando PostgreSQL. MongoUsuarioRepository implementa com MongoDB. InMemoryUsuarioRepository implementa com dicionario em memoria (para testes).
Tipos de ports: Ports de entrada (driving): como o mundo externo chama o dominio (REST API, CLI, eventos). Ports de saida (driven): como o dominio acessa recursos externos (banco, APIs, email).
Estrutura de pastas
src/
domain/ # Nucleo — ZERO dependencias externas
entities/ # Entidades de dominio (Usuario, Pedido)
services/ # Logica de negocio (RegistrarUsuario, ProcessarPedido)
ports/ # Interfaces (UsuarioRepository, EmailService)
exceptions/ # Excecoes de dominio (UsuarioNaoEncontrado)
adapters/ # Implementacoes concretas
inbound/ # Adapters de entrada
rest/ # Controllers REST
cli/ # Comandos CLI
graphql/ # Resolvers GraphQL
outbound/ # Adapters de saida
database/ # Implementacoes de Repository
email/ # Implementacao de EmailService
http/ # Clientes HTTP para APIs externas
config/ # Composicao e injecao de dependencias
Exemplo pratico: cadastro de usuario
# domain/ports/usuario_repository.py
from abc import ABC, abstractmethod
class UsuarioRepository(ABC):
@abstractmethod
def salvar(self, usuario):
pass
@abstractmethod
def buscar_por_email(self, email):
pass
# domain/services/registrar_usuario.py
class RegistrarUsuarioService:
def __init__(self, usuario_repo, email_service):
self.usuario_repo = usuario_repo
self.email_service = email_service
def executar(self, nome, email, senha):
# Verificar se email ja existe
existente = self.usuario_repo.buscar_por_email(email)
if existente:
raise EmailJaCadastrado(email)
# Criar usuario
usuario = Usuario(nome=nome, email=email, senha_hash=hash_senha(senha))
self.usuario_repo.salvar(usuario)
# Enviar email de boas-vindas
self.email_service.enviar_boas_vindas(usuario)
return usuario
# adapters/outbound/database/postgres_usuario_repo.py
class PostgreSQLUsuarioRepository(UsuarioRepository):
def __init__(self, session):
self.session = session
def salvar(self, usuario):
self.session.add(usuario)
self.session.commit()
def buscar_por_email(self, email):
return self.session.query(UsuarioModel).filter_by(email=email).first()
# adapters/outbound/database/in_memory_usuario_repo.py (para testes)
class InMemoryUsuarioRepository(UsuarioRepository):
def __init__(self):
self.usuarios = {}
def salvar(self, usuario):
self.usuarios[usuario.email] = usuario
def buscar_por_email(self, email):
return self.usuarios.get(email)
Testando sem banco de dados
A grande vantagem: testes unitarios rapidos e isolados:
def test_registrar_usuario_com_sucesso():
repo = InMemoryUsuarioRepository()
email_service = FakeEmailService()
service = RegistrarUsuarioService(repo, email_service)
usuario = service.executar(“Joao”, “joao@email.com”, “senha123”)
assert usuario.nome == “Joao”
assert repo.buscar_por_email(“joao@email.com”) is not None
assert email_service.emails_enviados == 1
def test_registrar_usuario_email_duplicado():
repo = InMemoryUsuarioRepository()
repo.salvar(Usuario(nome=”Maria”, email=”maria@email.com”, senha_hash=”xxx”))
service = RegistrarUsuarioService(repo, FakeEmailService())
with pytest.raises(EmailJaCadastrado):
service.executar(“Outra Maria”, “maria@email.com”, “senha456”)
Sem Docker, sem banco de dados, sem setup complexo. Os testes rodam em milissegundos.
Quando usar e quando nao usar
Use quando: o projeto tem logica de negocio complexa, a equipe e grande e precisa trabalhar em paralelo, voce antecipa mudancas de infraestrutura (troca de banco, framework), testabilidade e prioridade.
Nao use quando: o projeto e um CRUD simples sem regras de negocio complexas, o time e pequeno (1-3 pessoas) e o projeto e de curta duracao, a overhead da separacao nao se justifica pelo escopo.
Tem um projeto em mente?
Somos especialistas em transformar ideias em produtos digitais. Apps, sites, automações e IA — vamos construir juntos.