Onion Architecture
Coloca o domain model rico no núcleo absoluto do sistema, com dependências que apontam exclusivamente para dentro — a infraestrutura fica na casca mais externa e nunca é conhecida pelas camadas internas.
Intenção
Posicionar o domain model — entidades com comportamento real e serviços de domínio — no centro absoluto do sistema, de forma que nenhuma camada interna precise saber que existe um banco de dados, um framework web ou qualquer tecnologia externa. A infraestrutura é tratada como detalhe substituível na casca mais externa.
A Onion Architecture foi descrita por Jeffrey Palermo em 2008 e representa uma evolução direta da Arquitetura em Camadas com inversão explícita das dependências. Enquanto a Arquitetura em Camadas clássica posiciona a infraestrutura "abaixo" do domínio como sua dependência natural, a Onion inverte essa relação: a infraestrutura fica "fora" e implementa interfaces definidas pelo domínio.
A ênfase central da Onion Architecture é o domain model rico: entidades que encapsulam comportamento e invariantes, serviços de domínio que expressam operações que não pertencem a nenhuma entidade individual, e interfaces de repositório definidas dentro do domínio — não na infraestrutura. Um domínio anêmico (entidades como meros repositórios de dados) anula o benefício principal da arquitetura.
Problema
A Onion Architecture resolve os mesmos problemas da Arquitetura em Camadas clássica, com ênfase especial em dois deles:
- Dependência do domínio para a infraestrutura: em sistemas com Arquitetura em Camadas sem inversão de dependência, entidades importam classes ORM, serviços de domínio injetam conexões de banco de dados, e trocar de tecnologia de persistência exige modificar o domínio. A Onion inverte essa relação: o domínio define a interface; a infraestrutura a implementa.
- Domínio anêmico: quando as regras de negócio migram para serviços de aplicação porque as entidades são tratadas como estruturas de dados passivas, o domínio perde expressividade. A Onion Architecture incentiva explicitamente que as entidades tenham comportamento — métodos que aplicam regras, lançam eventos, validam invariantes.
- Dificuldade de testar o domínio isoladamente: quando o domínio depende de infraestrutura concreta, testar uma regra de negócio exige subir toda a stack. Com o domínio no centro e interfaces definidas por ele, qualquer camada pode ser substituída por um dublê de teste simples.
Estrutura
A estrutura é visualizada como camadas concêntricas de uma cebola. O centro contém as políticas mais estáveis; a casca externa contém os detalhes mais voláteis.
╔═══════════════════════════════════════════════════════════════════╗
║ INFRAESTRUTURA (casca mais externa) ║
║ Banco de dados, frameworks, UI, serviços externos, filas ║
║ Implementa interfaces definidas pelas camadas internas ║
║ ┌─────────────────────────────────────────────────────────────┐ ║
║ │ SERVIÇOS DE APLICAÇÃO │ ║
║ │ Casos de uso, orquestração, autorização, transações │ ║
║ │ Conhece o domínio; não conhece infraestrutura concreta │ ║
║ │ ┌───────────────────────────────────────────────────────┐ │ ║
║ │ │ SERVIÇOS DE DOMÍNIO │ │ ║
║ │ │ Operações de domínio que envolvem múltiplas │ │ ║
║ │ │ entidades; interfaces de repositório │ │ ║
║ │ │ ┌─────────────────────────────────────────────────┐ │ │ ║
║ │ │ │ DOMAIN MODEL (núcleo) │ │ │ ║
║ │ │ │ Entidades com comportamento, Value Objects │ │ │ ║
║ │ │ │ Invariantes, eventos de domínio │ │ │ ║
║ │ │ │ Sem nenhuma dependência externa │ │ │ ║
║ │ │ └─────────────────────────────────────────────────┘ │ │ ║
║ │ └───────────────────────────────────────────────────────┘ │ ║
║ └─────────────────────────────────────────────────────────────┘ ║
╚═══════════════════════════════════════════════════════════════════╝
Regra de dependência: todas as setas apontam para o centro.
A infraestrutura implementa interfaces do domínio — nunca o contrário.
O domínio não tem nenhum import de nenhuma camada externa.
As camadas da Onion
- Domain Model (núcleo): entidades com comportamento real, Value Objects imutáveis, invariantes de domínio e eventos de domínio. Não tem nenhuma dependência de nada externo. É o código mais estável do sistema.
- Serviços de Domínio: operações de domínio que envolvem múltiplas entidades ou que não pertencem naturalmente a nenhuma delas. Também é aqui que vivem as interfaces de repositório — definidas pelo domínio, implementadas pela infraestrutura.
- Serviços de Aplicação: casos de uso da aplicação. Orquestram entidades e serviços de domínio, aplicam autorização por caso de uso, gerenciam transações e convertem DTOs de entrada/saída. Conhecem o domínio, mas não conhecem infraestrutura concreta.
- Infraestrutura (casca): implementações concretas de repositórios, controllers HTTP, adaptadores de serviços externos, filas. A camada mais volátil — trocável sem impacto no domínio.
Como funciona — domínio sem imports de infraestrutura
O trecho abaixo ilustra o princípio central: o domínio define suas próprias interfaces de repositório e não importa nada de infraestrutura. A casca externa implementa essas interfaces e aponta para dentro.
// ── DOMAIN MODEL (núcleo — zero imports externos) ────────────
class Conta {
private saldo: number;
constructor(
readonly id: string,
saldoInicial: number,
) {
if (saldoInicial < 0) throw new Error('Saldo inicial não pode ser negativo');
this.saldo = saldoInicial;
}
debitar(valor: number): void {
if (valor <= 0) throw new Error('Valor de débito deve ser positivo');
if (valor > this.saldo) throw new Error('Saldo insuficiente');
this.saldo -= valor;
}
creditar(valor: number): void {
if (valor <= 0) throw new Error('Valor de crédito deve ser positivo');
this.saldo += valor;
}
get saldoAtual(): number { return this.saldo; }
}
// ── SERVIÇO DE DOMÍNIO — define a interface do repositório ───
// A interface vive DENTRO do domínio; a infra a implementa.
interface ContaRepository {
buscarPorId(id: string): Promise<Conta | null>;
salvar(conta: Conta): Promise<void>;
}
class TransferenciaService {
constructor(private readonly contas: ContaRepository) {}
async transferir(origemId: string, destinoId: string, valor: number): Promise<void> {
const origem = await this.contas.buscarPorId(origemId);
const destino = await this.contas.buscarPorId(destinoId);
if (!origem || !destino) throw new Error('Conta não encontrada');
origem.debitar(valor);
destino.creditar(valor);
await this.contas.salvar(origem);
await this.contas.salvar(destino);
}
}
// ── INFRAESTRUTURA (casca) — implementa a interface do domínio ──
// Depende do domínio; o domínio nunca depende dela.
class PostgresContaRepository implements ContaRepository {
async buscarPorId(id: string): Promise<Conta | null> {
// lê do banco; mapeia para a entidade de domínio
return null; // placeholder
}
async salvar(conta: Conta): Promise<void> {
// persiste no banco
}
}
O que torna este código Onion: Conta e
TransferenciaService não importam nada de fora do domínio.
ContaRepository é uma interface declarada dentro do domínio.
PostgresContaRepository fica na infraestrutura e importa o
domínio — nunca o contrário. Trocar o banco de dados é criar uma nova
implementação de ContaRepository; o domínio não muda.
Quando usar
- Domínios ricos com comportamento complexo: a Onion brilha quando há regras de negócio substanciais a modelar — validações de invariante, cálculos de domínio, estados de ciclo de vida de entidades. Quanto mais rico o domínio, mais o investimento nas camadas internas se paga.
- Quando a testabilidade do domínio é crítica: com o domínio no núcleo e interfaces de repositório definidas por ele, é possível testar toda a lógica de negócio com implementações in-memory simples, sem banco real, sem framework, com execução instantânea.
- Projetos com alta probabilidade de troca de infraestrutura: quando há planos de migrar de banco de dados, de provider de e-mail ou de sistema de pagamento, a separação entre domínio e infraestrutura permite que essa troca aconteça sem impacto nas regras de negócio.
- Sistemas de longa vida com equipe crescente: as fronteiras explícitas entre camadas reduzem o acoplamento acidental e facilitam o onboarding — o domínio é o vocabulário compartilhado da equipe, e a arquitetura garante que ele permaneça limpo.
Quando evitar
- Domínios anêmicos ou CRUDs puros: quando o sistema é essencialmente um formulário de persistência sem regras de negócio complexas, as camadas internas da Onion ficam vazias e o resultado é burocracia arquitetural sem benefício. O principal ganho da Onion é o domain model rico — sem ele, não há o que proteger.
- Equipes sem familiaridade com DDD ou inversão de dependência: sem o entendimento de que a interface de repositório deve ficar no domínio (não na infraestrutura), a Onion costuma ser implementada como Arquitetura em Camadas com dependências invertidas no sentido errado.
- Protótipos e MVPs: o custo de estruturar bem as camadas internas é real. Em fases de descoberta, comece simples e refatore quando o domínio mostrar sua complexidade real.
Prós e contras
Prós
- Domain model completamente isolado de frameworks, banco de dados e qualquer tecnologia externa.
- Testabilidade total do domínio sem infraestrutura: repositórios in-memory, serviços mockados, execução rápida.
- Substituição de infraestrutura sem impacto no domínio — trocar o banco é trocar apenas a implementação na casca externa.
- Ênfase no domain model rico incentiva entidades com comportamento real, aumentando a expressividade e a manutenibilidade.
- Compatível e convergente com Clean Architecture e Arquitetura Hexagonal — equipes familiarizadas com qualquer das três transitam facilmente.
Contras
- Overhead para domínios simples: as camadas concêntricas têm custo de organização e configuração de injeção de dependência.
- Domínio anêmico anula o benefício central: se as entidades não têm comportamento, o investimento nas camadas internas não se paga.
- A distinção entre serviço de domínio e serviço de aplicação não é sempre óbvia e pode gerar debates sobre onde colocar cada peça de lógica.
- Injeção de dependência obrigatória para compor as camadas — frameworks DI ajudam, mas adicionam complexidade de configuração.
Armadilhas comuns
1. Domínio anêmico — a armadilha central
O erro mais comum e mais destrutivo na Onion Architecture é construir um domínio onde as entidades são apenas classes de dados com getters e setters, e toda a lógica fica nos serviços de aplicação. Um domínio assim derrota o propósito da arquitetura: as camadas internas ficam vazias de valor, e o benefício de isolá-las é zero.
Entidades de domínio devem ter métodos que aplicam regras de negócio, lançam exceções quando invariantes são violadas e encapsulam o comportamento que seria natural perguntar para esse objeto. Se a resposta para "quem valida isso?" é sempre "o serviço de aplicação", o domínio está anêmico.
2. Interface de repositório fora do domínio
A interface de repositório deve ser declarada dentro do domínio (ou dos
serviços de domínio), não na infraestrutura. Se ContaRepository
está no pacote de infraestrutura e o domínio importa esse pacote para usar
a interface, a dependência foi invertida no sentido errado — o domínio
voltou a depender da infraestrutura. A interface vive dentro; a implementação
concreta fica fora e implementa a interface.
Regra prática: se você copiar apenas as pastas de domínio e serviços de domínio para outro projeto, o código deve compilar sem erros. Qualquer import que impeça isso revela uma dependência que não deveria existir.
3. Confundir serviço de domínio com serviço de aplicação
Serviços de domínio encapsulam operações que envolvem múltiplas entidades
e expressam conceitos do vocabulário do domínio (ex.: TransferenciaService,
PrecificacaoService). Serviços de aplicação orquestram esses
serviços de domínio para realizar casos de uso específicos da aplicação,
gerenciam transações e convertem DTOs. Colocar lógica de domínio no serviço
de aplicação é o mesmo que criar um domínio anêmico — a lógica fica acessível,
mas no lugar errado.
4. Infraestrutura dentro do domínio
Entidades que herdam de classes ORM (Eloquent Model, JPA Entity com anotações), ou serviços de domínio que recebem conexões de banco de dados por injeção, violam a fronteira mais importante da Onion. Esse acoplamento destrói a testabilidade e cria dependências de ciclo de vida que dificultam a evolução do domínio independentemente da tecnologia escolhida.
Arquiteturas e padrões relacionados
- Clean Architecture
- Arquitetura Hexagonal (Ports & Adapters)
- Arquitetura em Camadas
- MVC
- Adapter (GoF)
A Clean Architecture é a evolução mais próxima da Onion Architecture. Ambas colocam o domínio no centro, invertem as dependências e tratam a infraestrutura como detalhe substituível. A Clean Architecture adiciona uma distinção mais formal entre entidades (regras de negócio empresariais) e casos de uso (regras de negócio da aplicação), e formaliza os boundaries explícitos entre camadas. Na prática, uma implementação Onion bem estruturada respeita os mesmos princípios da Clean Architecture.
A Arquitetura Hexagonal compartilha o mesmo princípio de inversão de dependência — o domínio define interfaces (portas), a infraestrutura implementa (adaptadores). A diferença de vocabulário é maior do que a diferença conceitual: Hexagonal fala em portas primárias (driving) e secundárias (driven); Onion fala em camadas concêntricas. Ambas são compatíveis e frequentemente implementadas juntas.
A Arquitetura em Camadas é a precursora direta da Onion. A Onion resolve o problema central da Arquitetura em Camadas clássica — a dependência natural do domínio para a infraestrutura — ao inverter explicitamente essa relação. A Onion pode ser vista como uma Arquitetura em Camadas com inversão de dependência aplicada sistematicamente na fronteira entre domínio e infraestrutura.