O parafuso e a rosca padronizada
A indústria não fabrica uma furadeira para cada marca de parafuso. Ela definiu roscas padrão, e os fabricantes de parafuso se adaptam a elas. Quem tem o problema a resolver definiu o encaixe; quem fornece a peça obedece. Se fosse ao contrário, cada fornecedor novo obrigaria a trocar a ferramenta.
O problema
Sem inversão, a regra de negócio importa o driver do banco, o SDK do provedor de e-mail e o cliente HTTP. Ela deixa de ser legível como negócio e passa a ser um roteiro de integrações.
Testar exige infraestrutura, mudar de fornecedor exige tocar no domínio, e o código mais valioso do sistema fica refém do mais substituível.
A parte que costuma passar despercebida é a segunda metade do princípio: a abstração pertence a quem a usa. Criar uma interface e colocá-la no pacote de infraestrutura não inverte nada — a dependência continua apontando para fora. A interface precisa viver junto do domínio, escrita no vocabulário dele.
Isso muda o desenho do contrato. Um repositório definido pelo domínio fala de salvar(pedido) e buscarPorCliente(id) — não de conexões, transações ou SQL. Se a interface parece a API do banco, ela foi escrita do lado errado.
Convém separar do que costuma ser confundido: injeção de dependência é uma técnica de fornecer colaboradores, e inversão de controle é sobre quem chama quem. Nenhuma das duas garante DIP. Dá para injetar uma classe concreta de infraestrutura e continuar com a dependência apontando na direção errada — o princípio é sobre a direção da seta, não sobre o mecanismo.
O princípio em escala de arquitetura
DIP é o único dos cinco princípios SOLID que se aplica igual da classe até a arquitetura inteira — é a regra que sustenta hexagonal, ports and adapters e clean architecture.
Em todas essas arquiteturas, a regra é a mesma: as dependências apontam para dentro, em direção ao domínio, e o núcleo não conhece nada do que está fora. As 'portas' são exatamente as abstrações declaradas pelo domínio; os 'adaptadores' são as implementações de infraestrutura.
O ganho prático mais imediato é a testabilidade. Com as portas no lugar, a suíte do domínio roda em milissegundos com implementações em memória, sem docker, sem migração, sem rede — e isso muda o ritmo de trabalho mais do que qualquer outra decisão de design.
O ganho de longo prazo é a substituibilidade real. Trocar de provedor de e-mail ou de banco deixa de ser projeto e passa a ser escrever um adaptador novo. Vale notar que essa troca acontece menos do que se imagina — o que quase sempre se paga é o primeiro ganho, não o segundo.
Dependência invertida
- CriarPedido declara e depende de RepositorioPedidos
- o teste injeta um repositório em memória
- trocar de banco é escrever outro adaptador
- o domínio não importa nada de infraestrutura
O custo é uma interface a mais por porta e um lugar onde tudo é montado (a composição). Aplicado sem critério, produz interfaces com um implementador só e indireção sem ganho.
Domínio / caso de usonão importa nada de fora
interface RepositorioPedidosa porta — declarada aqui dentro
Infraestruturaconhece o domínio, e não o contrário
PostgresPedidosadaptador de produção
RepoEmMemoriaadaptador de teste
Navegue pelas camadas
Camada 1/3Política (alto nível)
use ← →Camada 1
Política (alto nível)
as regras, sem saber de infraestrutura
Contém o que dá valor ao sistema e muda por motivo de negócio. Não importa driver, framework nem SDK — se importar, a inversão não aconteceu.
No código
class CriarPedido { constructor(private repo: RepositorioPedidos) {} }Se violar: um `import` de biblioteca de banco dentro do caso de uso já denuncia a dependência na direção errada.
Estrutura
Código
// ANTES: a regra de negocio depende do detalhe concreto
class PostgresPedidos {
salvar(p: unknown): void { console.log("INSERT ..."); }
}
class CriarPedidoRuim {
private repo = new PostgresPedidos(); // acoplado ao banco
executar(dados: unknown): void {
this.repo.salvar(dados); // testar exige um Postgres
}
}
// DEPOIS: a abstracao pertence a quem USA
// (declarada junto do caso de uso, no dominio)
interface RepositorioPedidos {
salvar(p: Pedido): void;
}
interface Pedido {
id: string;
total: number;
}
class CriarPedido {
// depende da abstracao; nao sabe que Postgres existe
constructor(private repo: RepositorioPedidos) {}
executar(p: Pedido): void {
if (p.total <= 0) throw new Error("total inválido");
this.repo.salvar(p);
}
}
// A infra implementa o contrato do dominio — a seta se inverteu
class PostgresPedidosOk implements RepositorioPedidos {
salvar(p: Pedido): void { console.log("INSERT INTO pedidos", p.id); }
}
// Em teste, sem banco nenhum
class RepoEmMemoria implements RepositorioPedidos {
readonly salvos: Pedido[] = [];
salvar(p: Pedido): void { this.salvos.push(p); }
}
const repo = new RepoEmMemoria();
new CriarPedido(repo).executar({ id: "1", total: 100 });
console.log(repo.salvos.length); // 1from abc import ABC, abstractmethod
# A abstracao pertence a quem USA (fica junto do caso de uso)
class RepositorioPedidos(ABC):
@abstractmethod
def salvar(self, p): ...
class CriarPedido:
# depende da abstracao; nao sabe que Postgres existe
def __init__(self, repo: RepositorioPedidos):
self._repo = repo
def executar(self, p):
if p["total"] <= 0:
raise ValueError("total invalido")
self._repo.salvar(p)
# A infra implementa o contrato do dominio — a seta se inverteu
class PostgresPedidos(RepositorioPedidos):
def salvar(self, p):
print("INSERT INTO pedidos", p["id"])
# Em teste, sem banco nenhum
class RepoEmMemoria(RepositorioPedidos):
def __init__(self):
self.salvos = []
def salvar(self, p):
self.salvos.append(p)
repo = RepoEmMemoria()
CriarPedido(repo).executar({"id": "1", "total": 100})
print(len(repo.salvos)) # 1Casos de uso
Domínio testável sem banco
Cenário
A suíte de testes levava oito minutos porque cada caso subia um container de banco e aplicava migrações.
Como o padrão entra
Os casos de uso passam a depender de portas de repositório; os testes injetam implementações em memória. A suíte de domínio cai para segundos e roda a cada salvamento.
Trade-off: Implementações em memória divergem do banco real em detalhes que importam (unicidade, ordenação, transação). Sem uma suíte de contrato rodando contra as duas, os testes rápidos passam a validar um comportamento que a produção não tem.
Troca de provedor de pagamento
Cenário
A empresa precisa migrar de gateway por questão de taxa, e a integração atual está espalhada por vários serviços.
Como o padrão entra
Uma porta de cobrança declarada pelo domínio e dois adaptadores permitem migrar por etapas, inclusive rodando os dois em paralelo para comparar resultados.
Trade-off: A porta precisa ser genérica o bastante para os dois provedores sem virar o menor denominador comum — e recursos exclusivos de um deles ficam de fora ou vazam por opções específicas.
Relógio e aleatoriedade injetados
Cenário
Regras que dependem de data ('vence em 30 dias') e de sorteio ('amostragem de 10%') são impossíveis de testar de forma determinística.
Como o padrão entra
Tempo e aleatoriedade viram portas. Em produção usam o relógio e o gerador reais; em teste, valores fixos. Regras temporais passam a ter teste confiável.
Trade-off: É invasivo: praticamente todo objeto que precisa da data passa a recebê-la, e o código fica mais verboso em troca de determinismo nos testes.
- 1
Declarar a porta no domínio
O caso de uso escreve a interface no vocabulário dele (`salvar(pedido)`), junto dele — não no pacote de infraestrutura.
- 2
Depender só da porta
O alto nível recebe a abstração por construtor. Nenhum `import` de driver, SDK ou framework entra no caso de uso.
- 3
Implementar o adaptador fora
Postgres, HTTP e fila implementam o contrato alheio. A seta de compilação aponta da infra para o domínio.
- 4
Injetar na composição
Produção pluga o adaptador real; teste pluga memória. A montagem é o único lugar que conhece os dois lados.
A interface no pacote errado
Compila, roda, passa no code review. E quebra depois.
Há uma interface, há injeção no construtor, o time diz que 'segue DIP' — mas o contrato vive na infra e fala SQL. A seta nunca inverteu; só ganhou um I no nome.
// "Temos DIP": existe uma interface... no pacote de infraestrutura.
// A seta de dependencia CONTINUA apontando para fora.
import type { PostgresPedidos } from "../infra/postgres-pedidos";
interface IPostgresPedidos {
// espelho da classe concreta — nomes de SQL vazaram
executarInsert(row: Record<string, unknown>): void;
}
class CriarPedido {
constructor(private repo: IPostgresPedidos) {} // depende de "infra"
executar(dados: Record<string, unknown>) {
this.repo.executarInsert(dados); // dominio fala SQL disfarçado
}
}
// Trocar de banco ainda toca o caso de uso: a abstração nasceu do lado errado.O que quebra
- No importO domínio importa de
../infra— a dependência ainda aponta para fora. - Ao trocar de bancoA interface carrega
executarInserte obriga o caso de uso a mudar junto com a tecnologia. - No testeO falso precisa imitar a API do Postgres, não a necessidade do domínio — e diverge em silêncio.
A saída: Mova a porta para o domínio e reescreva no vocabulário do negócio: salvar(p: Pedido). A infra implementa esse contrato; o caso de uso deixa de saber que Postgres existe.
Armadilhas
Interface no pacote errado
Criar
RepositorioPedidosdentro do módulo de infraestrutura é o erro mais comum e o mais invisível: o código ganha uma interface, parece seguir o princípio, e a dependência continua apontando para fora. A abstração precisa morar junto de quem a usa — é isso que inverte a seta.Abstrair tudo por precaução
Uma interface para cada classe, com um implementador cada, transforma navegar no código numa caçada e não entrega substituibilidade nenhuma. Abstraia as fronteiras que realmente separam política de detalhe: banco, rede, relógio, fila. O resto pode ser concreto.
Porta modelada pela tecnologia
Se a interface tem
executarQuery,abrirTransacaooucommit, ela é a API do banco com outro nome. O domínio passa a depender de um conceito de infraestrutura disfarçado, e trocar de tecnologia continua exigindo mexer nele — a inversão é só aparente.
Quando usar × quando evitar
Use quando
3- A regra de negócio precisa ser testável sem infraestrutura.
- Detalhes externos podem mudar de provedor ou tecnologia.
- O domínio deve ser legível sem conhecimento de framework.
Evite quando
3- Scripts e ferramentas pequenas.
- Abstrações sem segunda implementação nem uso em teste.
- Espelhar bibliotecas inteiras atrás de interfaces próprias.
Testar o que ficou
3 armadilhas de DIP — Inversão de Dependência viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.
Onde isto aparece de verdade
Você já usa isto, talvez sem saber o nome.
injeção de dependência (NestJS)O container injeta implementações via construtor, e suas classes dependem de interfaces — o detalhe concreto é escolhido de fora, não lá dentro.React ContextO componente consome um contrato exposto pelo Provider sem saber quem o fornece; a implementação é injetada acima na árvore.ports do HexagonalO domínio declara a interface do repositório e o adaptador de banco a implementa: a dependência aponta para a abstração, não para o Postgres.
O padrão em uma linha
Sem nome de domínio, sem cerimônia. Só a ideia.
// Alto nível depende de abstração, não de SmtpClient.
constructor(private mail: PortaEmail) {}O que ele cobra
1 salto a mais entre a chamada e o efeito.
- Uma abstração a mais entre o cliente e a implementação, com a injeção a conectar os dois
- A indireção dificulta seguir do uso até o código concreto que roda de fato
Não vale se: a implementação é única e estável e não precisa ser trocada nem testada isoladamente — depender dela direto é mais simples.
Onde isto aparece
Continue por aqui
- Arquitetura HexagonalIsola o domínio no centro e o conecta ao mundo externo por portas (interfaces) e adaptadores (implementações), tornando infraestrutura um detalhe plugável.
- OCP — Aberto/FechadoMódulos devem ser abertos para extensão e fechados para modificação: adicionar comportamento novo deveria significar escrever código novo, não reabrir e alterar o que já funciona.
- AdapterConverte a interface de uma classe em outra que o cliente espera, permitindo que classes incompatíveis trabalhem juntas.