Aula 01 · 70–90 minutos

O que é Clean Architecture, e por quê

Aula 1 — Injeção de Dependência, o Dependency Inversion Principle, a Regra de Dependência, as quatro camadas, e a comparação com MVC/Active Record.

Resultado e mapa da aula

Ao final, você vai conseguir olhar para uma função que mistura regra de negócio com banco de dados e HTTP, apontar exatamente onde ela viola a Regra de Dependência, e reescrevê-la usando Injeção de Dependência. Você também vai saber a diferença entre o princípio (Dependency Inversion Principle) e a técnica que o viabiliza (Dependency Injection) — os dois são frequentemente confundidos pelo nome parecido.

  1. Partimos de um handler real, que mistura regra e infraestrutura.
  2. Isolamos a regra com Injeção de Dependência — uma técnica pequena, aplicada a uma função só.
  3. Generalizamos essa mesma ideia para o projeto inteiro: a Regra de Dependência.
  4. Nomeamos o princípio formal por trás dela: o Dependency Inversion Principle.
  5. Vemos as quatro camadas em ação, e comparamos tudo isso com o MVC que você já conhece.

O problema que ela resolve

Imagine um sistema onde a lógica de "aprovar um pedido" está espalhada dentro de um controller HTTP, misturada com chamadas diretas ao banco:

func AprovarPedidoHandler(w http.ResponseWriter, r *http.Request) {
    pedido := db.Query("SELECT * FROM pedidos WHERE id = ?", id)
    if pedido.Total > 10000 && !pedido.Cliente.VIP {
        w.WriteHeader(403)
        return
    }
    db.Exec("UPDATE pedidos SET status = 'aprovado' WHERE id = ?", id)
    enviarEmail(pedido.Cliente.Email)
}

Isso funciona — até você precisar testar a regra "pedido acima de R$10.000 só é aprovado automaticamente se o cliente for VIP" sem subir um banco de dados de verdade; trocar o banco sem reescrever a regra; reusar a mesma regra numa CLI administrativa, além da API HTTP; ou explicar essa regra para alguém do time de produto sem essa pessoa precisar ler SQL misturado com HTTP.

O problema não é o SQL nem o HTTP em si — é que a regra de negócio ("quando um pedido pode ser aprovado") está acoplada a decisões técnicas (qual banco, qual protocolo, qual biblioteca de e-mail) que não têm nada a ver com a regra em si. Qualquer mudança num detalhe técnico arrisca quebrar a regra, e qualquer teste da regra arrasta junto toda a infraestrutura.

Clean Architecture é uma forma de organizar o código para que isso não aconteça: a regra de negócio fica isolada, e os detalhes técnicos ficam "plugáveis" em volta dela.

Injeção de Dependência

Antes de chegar em Clean Architecture propriamente dita, precisamos de uma peça menor: Injeção de Dependência (DI). É uma técnica pequena, mas é o mecanismo que faz tudo o resto da aula funcionar — vale entender bem antes de seguir.

Volte ao handler de "aprovar pedido". Repare que ele cria sua própria conexão com o banco, na hora:

func AprovarPedidoHandler(w http.ResponseWriter, r *http.Request) {
    db := postgres.Connect(dbConnString) // cria a própria dependência
    pedido := db.Query("SELECT * FROM pedidos WHERE id = ?", id)
    // ...
}

Isso é o que chamamos de dependência criada internamente: a função decide, sozinha, qual implementação de banco usar, e a instancia diretamente dentro dela. O problema é sutil, mas caro: para testar essa função, você é obrigado a arrastar um Postgres de verdade junto — não tem como "enganar" a função e dar a ela um banco falso, porque ela nunca aceita um banco como entrada, ela sempre cria o seu.

Injeção de Dependência inverte isso: em vez da função criar a dependência, alguém de fora cria e injeta — passa como parâmetro:

func AprovarPedido(db PedidoRepository, id string) error {
    pedido := db.Buscar(id) // recebe a dependência pronta, de fora
    // ...
}

// em produção:
AprovarPedido(postgres.NovoRepositorio(), id)

// em teste:
AprovarPedido(fakeRepositorioComPedidoDeTeste, id)

A função AprovarPedido não sabe, e não precisa saber, se db é um Postgres de verdade ou uma implementação falsa criada só para o teste. Ela só sabe que recebe algo que responde à interface PedidoRepositoryBuscar retorna um pedido. Quem decide qual implementação usar é quem chama a função, não a função em si.

Comparação entre função sem e com injeção de dependênciaSem injeção de dependência, AprovarPedidoHandler cria seu próprio Postgres e depende de si mesma. Com injeção de dependência, AprovarPedido recebe "db" de fora — quem chama decide qual implementação usar.SEM injeção de dependênciaAprovarPedidoHandlercria seu próprio Postgresdepende de si mesmaCOM injeção de dependênciaAprovarPedido(db, id)recebe "db" de foraquem chama decideimplementação escolhida por fora

À esquerda, a função é dona da decisão de qual implementação usar — e fica presa a ela. À direita, quem chama a função decide, e a função só depende de uma interface.

Isso importa por dois motivos, os mesmos que vão reaparecer na Clean Architecture inteira: testabilidade — em teste, injeta-se uma implementação falsa no lugar da real, sem precisar de infraestrutura de verdade; e flexibilidade — trocar a implementação real por outra não exige tocar em quem a usa, só em quem a injeta.

Guarde essa ideia: quem usa a dependência não decide qual implementação usar; quem chama, decide. A Regra de Dependência da Clean Architecture, a seguir, é exatamente essa mesma ideia — só que aplicada ao projeto inteiro, não a uma função isolada.

A Regra de Dependência

Uncle Bob desenha a arquitetura como círculos concêntricos — camadas dentro de camadas. A regra que sustenta tudo: dependências de código-fonte só podem apontar para dentro. Nada em um círculo interno pode saber absolutamente nada sobre algo em um círculo mais externo.

Círculos concêntricos da Clean ArchitectureQuatro camadas concêntricas, da mais externa à mais interna: Frameworks e Drivers, Interface Adapters, Use Cases, Entities. A Regra de Dependência aponta sempre para dentro.Frameworks & DriversInterface AdaptersUse CasesEntities(regras de negócio)Regra deDependência

Entities não sabem que Use Cases existem; Use Cases não sabem que HTTP ou SQL existem.

Na prática: o código que representa "o que o negócio faz" nunca importa, referencia, ou sabe da existência de um framework web, uma biblioteca de banco de dados, ou uma tabela SQL. É o contrário: os detalhes técnicos é que dependem das regras de negócio, nunca o inverso. Isso importa por dois motivos concretos: testabilidade — como a regra de negócio não conhece banco de dados nem HTTP, você testa "pedido acima de R$10.000 sem VIP é rejeitado" com um teste unitário puro, em milissegundos, sem subir nada; e substituibilidade — trocar Postgres por outra coisa, ou a API REST por gRPC, vira uma mudança isolada na camada externa. A regra de negócio nem percebe.

O Dependency Inversion Principle

A Regra de Dependência que você acabou de ver não foi inventada do zero por Uncle Bob para a Clean Architecture. Ela é a aplicação de um princípio mais antigo e mais geral, o "D" de SOLID — um conjunto de cinco princípios de design orientado a objetos, descritos por Robert C. Martin. Nesta aula, só o "D" nos interessa; os outros quatro (S, O, L, I) ficam para uma aula futura, quando houver código real que os motive.

O Dependency Inversion Principle (DIP) tem duas partes: (1) módulos de alto nível não devem depender de módulos de baixo nível — ambos devem depender de abstrações; (2) abstrações não devem depender de detalhes — detalhes devem depender de abstrações.

Traduzindo com o exemplo que você já viu: AprovarPedido (alto nível — a regra de negócio) e Postgres (baixo nível — o detalhe técnico) não devem depender um do outro diretamente. Os dois devem depender de algo no meio: a abstração PedidoRepository. É exatamente o que o diagrama da Regra de Dependência, acima, já mostrou — só que agora você sabe o nome do princípio geral por trás dele.

Comparação entre dependência direta e Dependency Inversion PrincipleSem DIP, AprovarPedido depende direto de Postgres. Com DIP, os dois dependem de uma abstração no meio, PedidoRepository — AprovarPedido a usa, Postgres a implementa.SEM DIPAprovarPedidoPostgresdepende diretoCOM DIPAprovarPedidoPedidoRepositoryPostgresimplementa

Sem DIP, alto nível depende direto de baixo nível. Com DIP, os dois dependem de uma abstração no meio — nenhum dos dois conhece o outro diretamente.

Um detalhe que vale reter: DIP não é o mesmo que Injeção de Dependência, mesmo com nomes parecidos. DIP é o princípio — a regra sobre quem deveria depender de quem. DI é a técnica — como, na prática, uma implementação concreta chega até quem precisa dela (via parâmetro, construtor). DIP é o "porquê"; DI é o "como". Você já viu os dois nesta aula, nessa ordem.

Agora a peça encaixa: a Regra de Dependência da Clean Architecture é o DIP aplicado sistematicamente a um projeto inteiro, camada por camada — não só a um par isolado de módulos como no exemplo acima.

As camadas, em ação

Os nomes variam entre autores, mas a ideia se repete em quatro níveis, do mais interno ao mais externo:

  • Entities — as regras de negócio mais gerais e estáveis. Não sabem nada sobre banco de dados ou HTTP.
  • Use Cases — orquestram as entidades para realizar uma ação específica do sistema (ex: "aprovar pedido").
  • Interface Adapters — traduzem dados entre o formato dos use cases e o formato externo (ex: um controller HTTP, um repositório de banco).
  • Frameworks & Drivers — a camada mais externa: o framework web, o driver do banco, a UI. Os detalhes mais fáceis de trocar.

Veja como o exemplo de "aprovar pedido" atravessaria essas camadas:

Fluxo de uma requisição de aprovar pedido pelas camadasHTTP Handler chama o Use Case AprovarPedido, que valida com a Entity Pedido e usa a interface PedidoRepository, implementada por Postgres na camada de Frameworks e Drivers.HTTP Handler(Interface Adapter)AprovarPedido(Use Case)Pedido(Entity)1. chama2. valida com3. usa ainterfaceinterfacePedidoRepositoryPostgres(Frameworks & Drivers)4. implementa

O Use Case define a interface PedidoRepository — ele é o dono do contrato. Quem implementa essa interface é o Postgres, lá fora. A dependência de código-fonte aponta da direita (Postgres) para o centro (a interface), embora o fluxo de execução vá da esquerda para a direita.

Repare no ponto 4 do diagrama: o Use Case é quem declara a interface PedidoRepository — não o banco. É essa inversão que faz o Postgres "depender" da regra de negócio, e não o contrário. Trocar Postgres por outro banco significa escrever uma nova implementação dessa mesma interface; o Use Case não muda uma linha. O ganho final: as regras de negócio (as camadas internas) podem ser testadas sem subir servidor HTTP nem banco de dados — porque literalmente não sabem que essas coisas existem.

Clean Architecture vs. MVC

Você já usou MVC (Model-View-Controller) em outras linguagens, então vale comparar diretamente. MVC também separa responsabilidades — mas não impõe uma Regra de Dependência explícita entre elas.

Na teoria, o Model do MVC deveria conter a regra de negócio. Na prática, em muitos frameworks (Rails, Django, Laravel), o "Model" é uma classe Active Record — ao mesmo tempo a regra de negócio e o mapeamento direto de uma tabela do banco. Ele herda de uma classe do ORM, tem métodos como salvar e buscar, e nada impede que validação de negócio entre num Controller que já está ali, com o banco a uma chamada de distância. Não há uma fronteira que o compilador ou o framework façam cumprir — só uma convenção que erode com o tempo e a pressão de prazos.

MVC tradicionalClean Architecture
Onde mora a regra de negócioDeveria ser o Model — na prática, vaza para o Controller ou fica misturada ao ORMEntities e Use Cases, isolados por design
O que a regra de negócio "sabe"Frequentemente sabe do ORM, da sessão HTTP, do frameworkNada sobre banco, HTTP ou framework — por definição
Fronteira entre camadasConvenção informal, fácil de violar sem perceberRegra de Dependência explícita, reforçada por interfaces
Testar a regra de negócioGeralmente exige banco de dados ou mocks pesados do frameworkTeste unitário puro, sem infraestrutura

Isso não quer dizer que MVC está "errado" — é um padrão de outra camada de preocupação (como estruturar a apresentação), e é perfeitamente possível escrever um Model MVC disciplinado que respeita a mesma Regra de Dependência. Clean Architecture é mais rígida: ela dá um nome e uma estrutura explícita para a fronteira que, em MVC, fica por conta da disciplina de quem escreve o código.

Prática guiada: identifique e corrija a violação

Raciocínio antes do código

Há duas regras de negócio escondidas nesta função: "um livro já emprestado não pode ser emprestado de novo" e "um usuário não pode ter mais de 3 empréstimos simultâneos". Nenhuma das duas tem relação com SQL, Postgres, ou qualquer detalhe técnico — são decisões que fariam sentido para alguém do time de produto, sem qualquer conhecimento de banco de dados. A função cria sua própria conexão internamente, exatamente como o handler de "aprovar pedido" no início desta aula: a regra de negócio está fisicamente no mesmo bloco de código que sabe SQL e Postgres, e não há como testar "usuário com 3 empréstimos ativos não pode pegar um quarto livro" sem subir um Postgres real.

Uma abordagem plausível, mas incorreta (1): mockar o pacote de banco globalmente

Uma tentação comum é usar uma variável de pacote substituível em teste:

var connectFunc = postgres.Connect // variável substituível em teste

func EmprestarLivro(livroID, usuarioID string) error {
    conn := connectFunc("postgres://localhost/biblioteca")
    // ...
}

Por que falha: isso resolve o sintoma (o teste roda sem banco real) mas não a causa. A função ainda depende, na sua própria implementação, do conceito de "conexão Postgres" — só que agora escondido atrás de uma variável global mutável. Esse padrão cria estado global compartilhado entre testes (go test -race frequentemente acusa corrida de dados nesse padrão, porque testes paralelos sobrescrevem a mesma variável), esconde a dependência real da função (quem lê a assinatura de EmprestarLivro não tem ideia de que ela depende de banco de dados), e não escala: cada nova dependência externa exigiria uma nova variável global substituível.

Uma abordagem plausível, mas incorreta (2): extrair só a validação, ainda recebendo a conexão concreta

func EmprestarLivro(livroID, usuarioID string) error {
    conn := postgres.Connect("postgres://localhost/biblioteca")
    defer conn.Close()
    return validarEExecutarEmprestimo(conn, livroID, usuarioID)
}

func validarEExecutarEmprestimo(conn *postgres.Conn, livroID, usuarioID string) error {
    // mesma lógica de antes, agora numa função separada
}

Por que falha: isso é uma refatoração cosmética — separa em duas funções, mas a segunda ainda recebe *postgres.Conn, um tipo concreto do driver Postgres. A regra de negócio continua acoplada a um detalhe técnico específico; para testá-la, ainda é preciso fornecer algo desse tipo concreto, e não existe forma padrão de "falsificar" um tipo de um driver de terceiros sem infraestrutura real ou bibliotecas pesadas de mock de banco (que testam a query SQL, não a regra de negócio). O problema estrutural não foi resolvido, só movido.

A solução correta: Injeção de Dependência via interface

// A regra de negócio declara o que precisa, não como é implementado.
type LivroRepository interface {
    BuscarLivro(id string) (Livro, error)
    BuscarUsuario(id string) (Usuario, error)
    RegistrarEmprestimo(livroID, usuarioID string) error
}

func EmprestarLivro(repo LivroRepository, livroID, usuarioID string) error {
    livro, err := repo.BuscarLivro(livroID)
    if err != nil {
        return err
    }
    if livro.Emprestado {
        return errors.New("livro já emprestado")
    }

    usuario, err := repo.BuscarUsuario(usuarioID)
    if err != nil {
        return err
    }
    if usuario.EmprestimosAtivos >= 3 {
        return errors.New("usuário já atingiu o limite de 3 empréstimos simultâneos")
    }

    return repo.RegistrarEmprestimo(livroID, usuarioID)
}

Em produção, alguém injeta uma implementação real (EmprestarLivro(postgres.NovoLivroRepository(conn), livroID, usuarioID)); em teste, injeta-se um fake sem qualquer banco de dados, uma struct comum que satisfaz LivroRepository devolvendo valores fixos.

Por que esta é a solução correta: a interface pertence à regra de negócio, não ao banco — LivroRepository é declarada onde EmprestarLivro vive, o Use Case é o dono do contrato, exatamente como PedidoRepository nesta aula (Dependency Inversion Principle aplicado: tanto a função quanto a futura implementação Postgres dependem da abstração, nenhum depende do outro diretamente). A assinatura da função revela suas dependências — ao ler EmprestarLivro(repo LivroRepository, ...), fica explícito que ela precisa de um repositório, sem ler o corpo inteiro. E o teste roda em microssegundos, sem infraestrutura.

Trade-offs: para um script descartável de uso único, ou um protótipo que será jogado fora em uma semana, esse nível de indireção pode ser desnecessário — o custo de escrever a interface e o fake pode superar o benefício. A extração vale a pena quando a regra de negócio é crítica o suficiente para justificar testá-la isoladamente, ou quando há expectativa real de trocar a implementação de persistência no futuro.

Verificação: rode go test -race ./... sobre o pacote — o teste com o fake deve passar sem qualquer variável de ambiente de banco de dados configurada, e deve continuar passando mesmo removendo o Postgres da máquina. Esse é o critério prático de que a regra de negócio está de fato isolada: ela nunca soube que Postgres existia.

Perguntas de entrevista e de mercado

"Explique a Regra de Dependência com suas próprias palavras, sem citar o Uncle Bob." Uma boa resposta evita decorar a definição e mostra que você entende o mecanismo: uma camada mais interna (como a regra de negócio) nunca importa, referencia ou conhece um tipo concreto de uma camada mais externa (como um driver de banco). Se a regra de negócio precisa de algo de fora, ela declara uma interface descrevendo o que precisa, e é a camada externa que implementa essa interface. Feche com um exemplo concreto — entrevistadores valorizam quem sai da teoria e mostra o mecanismo funcionando em código.

"Qual a diferença entre Dependency Injection e um Service Locator?" Em DI, a dependência chega de fora, explicitamente, via parâmetro ou construtor — a assinatura da função revela exatamente do que ela precisa. Em um Service Locator, o código pede a dependência a um registro global escondido dentro do corpo da função — a assinatura não revela a dependência, e o acoplamento à existência do locator continua. A maioria dos autores modernos considera Service Locator um anti-padrão parcial: resolve o teste de unidade superficialmente, mas esconde dependências.

"Você herdou um projeto MVC com regra de negócio dentro dos Controllers. Como você migraria isso, incrementalmente, sem reescrever tudo de uma vez?" Descreva passos incrementais: identificar uma regra de negócio específica e crítica dentro de um Controller; extraí-la para uma função pura que recebe os dados de que precisa como parâmetros, sem importar nada do framework web; fazer o Controller chamar essa função; escrever um teste unitário provando que ela não precisa mais do framework para ser testada; repetir para a próxima regra crítica. Mencione que isso não exige reescrita completa, e que o critério de "pronto" é "este código agora testa sem subir infraestrutura".

"Clean Architecture sempre vale a pena? Quando você NÃO a recomendaria?" Uma resposta madura reconhece o trade-off: mais camadas e interfaces custam código real, tempo de navegação e uma curva de aprendizado para o time. Isso compensa quando o domínio de negócio é complexo, o time é grande, o projeto tem vida longa esperada, ou há requisitos reais de trocar infraestrutura ao longo do tempo. Não compensa tão claramente em protótipos descartáveis, scripts de vida curta, ou times muito pequenos onde a disciplina de código já é alta e a arquitetura simples não está causando dor real. Cite um sinal concreto de quando vale a pena: quando testar a regra de negócio começa a exigir subir infraestrutura, ou quando trocar um detalhe técnico começa a quebrar regras de negócio sem relação aparente.

Recupere da memória: 10 questões

Recupere da memória: 10 questões

Segundo a Regra de Dependência, o que NÃO pode acontecer?

Numa função que usa Injeção de Dependência corretamente, quem decide qual implementação concreta será usada?

Por que uma função que cria sua própria conexão de banco de dados internamente é difícil de testar sem infraestrutura real?

Qual a diferença entre Dependency Inversion Principle (DIP) e Dependency Injection (DI)?

Em qual camada da Clean Architecture a regra 'pedido acima de R$10.000 sem VIP é rejeitado' deveria morar?

Por que o padrão Active Record, comum em frameworks como Rails ou Laravel, tende a misturar regra de negócio com detalhes de persistência?

O que Hexagonal Architecture (Ports & Adapters) tem a ver com Clean Architecture?

No diagrama de fluxo 'aprovar pedido', quem declara a interface PedidoRepository no código-fonte?

Qual é a crítica mais séria e equilibrada à Clean Architecture apresentada nesta aula?

Qual é o sinal mais concreto de que uma regra de negócio está corretamente isolada, segundo esta aula?

Feche o modelo mental

Clean Architecture começa numa técnica pequena (Injeção de Dependência) e a generaliza para o projeto inteiro (a Regra de Dependência), sustentada por um princípio formal mais antigo (o Dependency Inversion Principle). O ganho não é estético: é a capacidade de testar regra de negócio sem infraestrutura, e de trocar detalhes técnicos sem arriscar a regra. Guarde o vocabulário das quatro camadas — Entities, Use Cases, Interface Adapters, Frameworks & Drivers — porque toda aula seguinte desta trilha se refere a elas pelo nome.

Próxima aula planejada: Interfaces em Go — satisfação implícita, onde declarar a interface, e a pegadinha do nil.

Referências primárias para aprofundar

Consulte também o glossário desta trilha e o registro de aprendizagem desta aula no repositório do curso.