Aula 04 · 60–75 minutos
Primeiro projeto Go real
Aula 4 — construindo aprovador-pedidos do zero: domain puro, use case que declara a interface, fake para teste, implementação em memória e injeção manual em main.go.
Resultado e mapa da aula
As três lições anteriores deram toda a teoria e mecânica necessárias: a Regra de Dependência, a satisfação implícita de interfaces, e como isso vira pastas e pacotes de verdade. Chegou a hora de juntar tudo num projeto pequeno, mas real: uma regra de negócio, um use case, um repositório fake para teste, e uma implementação real — cada peça no seu pacote, rodando de verdade.
- Escrevemos o domínio (
domain): uma regra de negócio pura, sem nenhum import externo. - Escrevemos o use case (
usecase): ele orquestra o domínio e declara a interface do repositório. - Testamos o use case com um fake escrito à mão, sem banco de dados.
- Escrevemos uma implementação real (
memoria), que satisfaz a interface sem importar quem a declarou. - Fechamos o ciclo em
main.go, o único lugar que conhece as três peças ao mesmo tempo.
O domínio: uma regra de negócio pura
Tudo começa na camada mais interna — Entities na nomenclatura da Lição 1. O exemplo: aprovar ou rejeitar um pedido conforme um valor máximo. Repare que não há import nenhum além da biblioteca padrão — nenhuma menção a banco de dados, HTTP, ou qualquer detalhe técnico.
// internal/domain/pedido.go
package domain
type Pedido struct {
ID string
Total float64
}
const limiteAprovacaoAutomatica = 1000.0
func (p Pedido) PodeAprovarAutomaticamente() bool {
return p.Total <= limiteAprovacaoAutomatica
}
Essa função é testável sozinha, sem nenhum setup — nem um teste de unidade precisa de mock aqui, porque não há nada externo para simular:
// internal/domain/pedido_test.go
package domain
import "testing"
func TestPodeAprovarAutomaticamente(t *testing.T) {
p := Pedido{ID: "1", Total: 500}
if !p.PodeAprovarAutomaticamente() {
t.Error("esperava aprovação automática para pedido de 500")
}
}
O use case: orquestra e declara a interface
O Use Case chama a regra de domínio e decide o que fazer com o resultado — mas ele também precisa persistir o pedido em algum lugar. Seguindo a convenção da Lição 2, é o use case quem declara a interface do repositório, porque é ele quem a usa.
// internal/usecase/aprovar_pedido.go
package usecase
import "github.com/allangrds/aprovador-pedidos/internal/domain"
type PedidoRepository interface {
Salvar(p domain.Pedido, aprovado bool) error
}
type AprovarPedido struct {
repo PedidoRepository
}
func NovoAprovarPedido(r PedidoRepository) *AprovarPedido {
return &AprovarPedido{repo: r}
}
func (uc *AprovarPedido) Executar(p domain.Pedido) error {
aprovado := p.PodeAprovarAutomaticamente()
return uc.repo.Salvar(p, aprovado)
}
Note a direção do import: usecase importa domain (a Regra de Dependência aponta para dentro), mas nada aqui importa um pacote de banco de dados. PedidoRepository é só uma lista de métodos — o mesmo padrão da Lição 2, agora dentro do projeto real.
O fake: testando o use case sem banco de dados
Para testar AprovarPedido.Executar, não é preciso nenhum banco de dados de verdade — só um tipo que satisfaça PedidoRepository. Como visto na Lição 2, isso é uma struct comum, escrita à mão, guardada em memória:
// internal/usecase/aprovar_pedido_test.go
package usecase
import (
"testing"
"github.com/allangrds/aprovador-pedidos/internal/domain"
)
type repositorioFake struct {
salvos map[string]bool
}
func (r *repositorioFake) Salvar(p domain.Pedido, aprovado bool) error {
r.salvos[p.ID] = aprovado
return nil
}
func TestExecutar_AprovaPedidoDentroDoLimite(t *testing.T) {
fake := &repositorioFake{salvos: map[string]bool{}}
uc := NovoAprovarPedido(fake)
err := uc.Executar(domain.Pedido{ID: "1", Total: 500})
if err != nil {
t.Fatalf("erro inesperado: %v", err)
}
if !fake.salvos["1"] {
t.Error("esperava pedido salvo como aprovado")
}
}
Esse teste roda em milissegundos, sem rede, sem banco de dados, sem containers. É o benefício concreto que a Lição 1 prometeu de forma abstrata: regra de negócio testável sem depender de infraestrutura.
A implementação real
Em produção, PedidoRepository seria implementada por algo que fala com Postgres — isso chega na Lição 11, quando o projeto tiver uma necessidade concreta de persistência real. Por enquanto, a implementação "real" abaixo guarda em memória, mas do lado de fora do teste, no seu próprio pacote — o suficiente para mostrar a peça faltante do quebra-cabeça sem introduzir um driver de banco de dados:
// internal/memoria/pedido_repository.go
package memoria
import "github.com/allangrds/aprovador-pedidos/internal/domain"
type PedidoRepository struct {
aprovados map[string]bool
}
func NovoPedidoRepository() *PedidoRepository {
return &PedidoRepository{aprovados: map[string]bool{}}
}
func (r *PedidoRepository) Salvar(p domain.Pedido, aprovado bool) error {
r.aprovados[p.ID] = aprovado
return nil
}
Repare: memoria.PedidoRepository nunca importa usecase — só implementa o método certo. É a mesma satisfação implícita da Lição 2, agora com um pacote de infra de verdade.
Fechando o ciclo: injeção manual em main.go
Falta uma peça: alguém precisa criar a implementação real e entregá-la ao use case. É aqui que a Injeção de Dependência da Lição 1 acontece na prática — sem nenhum framework de DI, só passando o valor como parâmetro:
// main.go
package main
import (
"fmt"
"github.com/allangrds/aprovador-pedidos/internal/domain"
"github.com/allangrds/aprovador-pedidos/internal/memoria"
"github.com/allangrds/aprovador-pedidos/internal/usecase"
)
func main() {
repo := memoria.NovoPedidoRepository()
aprovarPedido := usecase.NovoAprovarPedido(repo)
pedido := domain.Pedido{ID: "1", Total: 750}
if err := aprovarPedido.Executar(pedido); err != nil {
fmt.Println("erro:", err)
return
}
fmt.Println("pedido processado")
}
main.go é o único lugar que conhece todas as peças ao mesmo tempo — domain, usecase e memoria. Isso é esperado: é a camada de Frameworks & Drivers da Lição 1, cujo trabalho é justamente montar (fazer o "wiring" de) as peças internas, que nunca se conhecem entre si.
A estrutura de pastas completa
Juntando tudo, seguindo o crescimento orgânico e o internal/ da Lição 3 — como este projeto não é uma biblioteca para terceiros importarem, tudo exceto main.go fica dentro de internal/:
aprovador-pedidos/
go.mod
main.go
internal/
domain/
pedido.go
pedido_test.go
usecase/
aprovar_pedido.go
aprovar_pedido_test.go
memoria/
pedido_repository.go
Três pacotes, uma direção de dependência só: usecase → domain, e memoria → domain. Nada aponta para usecase. É a Regra de Dependência da Lição 1, o mecanismo de pacotes da Lição 3, e a satisfação implícita da Lição 2 — as três lições anteriores, num projeto que compila e roda.
main é o único pacote que conhece usecase, memoria e domain ao mesmo tempo — ele injeta o repositório concreto no use case. usecase e memoria importam domain, mas nunca um ao outro: são pacotes irmãos, cada um conhecendo só a regra de negócio que consomem, nunca a implementação vizinha.
Prática guiada: adicione um terceiro pacote de implementação
Raciocínio antes do código
A tarefa pede, essencialmente, para repetir o mesmo padrão que internal/memoria já demonstra — uma struct nova, num pacote novo, que satisfaz PedidoRepository por ter o método Salvar com a assinatura certa. O que a distingue de memoria é só o destino dos dados (arquivo em disco em vez de mapa em RAM); a forma de acoplamento com o resto do sistema deveria ser idêntica.
Uma abordagem plausível, mas incorreta: importar usecase para "documentar" a interface satisfeita
// internal/arquivo/pedido_repository.go
package arquivo
import (
"encoding/json"
"os"
"github.com/allangrds/aprovador-pedidos/internal/domain"
"github.com/allangrds/aprovador-pedidos/internal/usecase" // desnecessário
)
type PedidoRepository struct {
caminho string
}
// assinatura documentada com o tipo do pacote usecase, "para deixar claro"
func (r *PedidoRepository) Salvar(p domain.Pedido, aprovado bool) error {
var _ usecase.PedidoRepository = r // checagem manual de satisfação
f, err := os.OpenFile(r.caminho, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
linha, _ := json.Marshal(map[string]any{"id": p.ID, "total": p.Total, "aprovado": aprovado})
_, err = f.Write(append(linha, '\n'))
return err
}
Por que falha: a linha var _ usecase.PedidoRepository = r não quebra nada tecnicamente, mas ela introduz um import de usecase dentro de arquivo que não é necessário — a satisfação implícita (Lição 2) já garante que *arquivo.PedidoRepository satisfaz usecase.PedidoRepository simplesmente por ter o método certo, sem checagem manual. Esse import extra cria uma dependência de código-fonte que não existia antes: agora arquivo "conhece" usecase, o que é exatamente o tipo de acoplamento invertido que a Lição 2 ensinou a evitar (quem implementa não deveria precisar importar quem consome). Em produção isso não quebra a compilação, mas cria uma dependência circular em potencial se um dia usecase precisasse importar algo de arquivo, e torna a árvore de dependências mais difícil de raciocinar durante manutenção.
A solução correta: pacote independente, sem importar usecase
// internal/arquivo/pedido_repository.go
package arquivo
import (
"encoding/json"
"fmt"
"os"
"github.com/allangrds/aprovador-pedidos/internal/domain"
)
type PedidoRepository struct {
caminho string
}
func NovoPedidoRepository(caminho string) *PedidoRepository {
return &PedidoRepository{caminho: caminho}
}
type registro struct {
ID string `json:"id"`
Total float64 `json:"total"`
Aprovado bool `json:"aprovado"`
}
func (r *PedidoRepository) Salvar(p domain.Pedido, aprovado bool) error {
f, err := os.OpenFile(r.caminho, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("abrir arquivo de pedidos: %w", err)
}
defer f.Close()
linha, err := json.Marshal(registro{ID: p.ID, Total: p.Total, Aprovado: aprovado})
if err != nil {
return fmt.Errorf("serializar pedido %s: %w", p.ID, err)
}
if _, err := f.Write(append(linha, '\n')); err != nil {
return fmt.Errorf("gravar pedido %s: %w", p.ID, err)
}
return nil
}
A mudança em main.go é uma linha só:
// main.go — antes
repo := memoria.NovoPedidoRepository()
// main.go — depois
repo := arquivo.NovoPedidoRepository("pedidos.jsonl")
Por que está correto: arquivo importa só domain — nunca usecase. A satisfação de usecase.PedidoRepository é verificada estruturalmente, no único ponto de uso: a linha em main.go que atribui repo ao parâmetro de usecase.NovoAprovarPedido. A troca de implementação exige mudar exatamente uma linha em main.go — nenhuma linha de domain ou usecase muda. Isso confirma na prática o que a Lição 1 prometeu de forma abstrata: um detalhe técnico (onde persistir) é plugável sem tocar na regra de negócio. O tipo registro — não domain.Pedido com tags json — mantém o formato de serialização como uma preocupação exclusiva de arquivo, o mesmo raciocínio de DTO que a Lição 9 nomeia mais adiante no curso, aplicado aqui de forma antecipada.
Verificação: escreva um teste em internal/arquivo/pedido_repository_test.go que chama Salvar com um arquivo temporário (os.CreateTemp) e depois lê o conteúdo de volta, conferindo que a linha JSON gravada bate com o pedido salvo. Rode go build ./... e confirme, com go list -deps ./internal/arquivo, que internal/usecase não aparece na lista de dependências do pacote arquivo — essa é a prova objetiva de que a direção de dependência está correta.
Perguntas de entrevista e de mercado
"Neste projeto de exemplo, por que main.go é o único arquivo que conhece domain, usecase e memoria ao mesmo tempo? Isso não seria um acoplamento ruim?" Distinga dois tipos de acoplamento: o acoplamento que a Clean Architecture proíbe é o das camadas internas dependerem de detalhes externos (domain ou usecase importando memoria ou um driver de banco). main.go é diferente — ele é a camada de Frameworks & Drivers, cujo único trabalho é "wiring": criar as implementações concretas e injetá-las nos use cases. Esse acoplamento é aceitável, e até necessário, porque é unidirecional e fica isolado num único ponto do sistema — se amanhã memoria virar postgres, só main.go muda uma linha; domain e usecase nunca precisam saber que essa troca aconteceu. Vale citar que esse padrão às vezes é chamado de "Composition Root" na literatura de DI.
"Como você testaria o use case AprovarPedido sem usar um banco de dados real, e por que isso é possível neste projeto?" Aponte para o fake em internal/usecase/aprovar_pedido_test.go: uma struct simples (repositorioFake) que satisfaz a interface PedidoRepository guardando dados num mapa em memória. O ponto a aprofundar é o "por quê": isso só é possível porque AprovarPedido depende da interface PedidoRepository, não de um tipo concreto como *sql.DB ou *postgres.PedidoRepository. Se o use case importasse diretamente um driver de banco, não haveria como substituí-lo por um fake sem reescrever o código de produção. Mencione também que esse teste roda em milissegundos, sem rede nem containers — o benefício prático e mensurável da arquitetura, não só um princípio abstrato.
"Este projeto usa injeção manual de dependências, sem nenhum framework de DI (como Wire ou Fx). Isso é uma limitação, ou uma escolha razoável?" Evite tratar "sem framework de DI" como deficiência. Em Go, projetos pequenos a médios costumam preferir injeção manual (como em main.go desta lição) porque ela é explícita, fácil de seguir com "vá para definição", e não introduz uma dependência externa nem geração de código. Frameworks de DI (Wire faz geração de código em tempo de compilação; Fx e Dig usam reflection em runtime) começam a valer a pena quando o grafo de dependências cresce muito — dezenas de componentes com dependências cruzadas — e a montagem manual em main.go ficaria longa e repetitiva. Cite esse critério de escala, em vez de recomendar uma ferramenta por hábito ou modismo.
"O que aconteceria se você adicionasse um método a PedidoRepository que só a implementação memoria usa, mas usecase nunca chama?" Esta pergunta testa se você entende o Interface Segregation Principle na prática, mesmo sem citar a sigla. Explique que isso violaria o ISP: a interface deveria conter só os métodos que os seus clientes (aqui, usecase.AprovarPedido) realmente usam. Adicionar um método usado só por uma implementação específica sugere que ele não pertence à interface do repositório — talvez pertença a um tipo concreto adicional, ou a uma segunda interface menor, dependendo de quem precisaria chamá-lo. Vale mencionar que esse raciocínio aparece formalizado mais adiante no curso, quando um segundo use case de consulta ganha sua própria interface em vez de engordar PedidoRepository.
Recupere da memória: 10 questões
Recupere da memória: 10 questões
Neste projeto, qual pacote é o único que importa domain, usecase e memoria ao mesmo tempo?
Por que domain/pedido.go pode ser testado sem nenhum mock ou setup especial?
Neste projeto, em qual pacote é declarada a interface PedidoRepository?
O que é repositorioFake, usado no teste de AprovarPedido.Executar?
O pacote memoria importa o pacote usecase para que PedidoRepository (a implementação) satisfaça a interface do mesmo nome?
Onde, exatamente, a Injeção de Dependência da Lição 1 acontece fisicamente neste projeto?
Qual afirmação descreve corretamente a direção de dependências entre os três pacotes internos (domain, usecase, memoria)?
Se este projeto precisasse trocar o armazenamento em memória por Postgres, quais arquivos precisariam mudar?
Onde mora a constante limiteAprovacaoAutomatica, e o que isso comunica sobre seu papel no sistema?
A estrutura de pastas deste projeto (domain sem dependências, memoria como subpacote-adapter) corresponde a qual padrão de mercado citado na lição?
Feche o modelo mental
domain é a única camada sem imports internos — a regra de negócio, testável sozinha. usecase importa domain e declara a interface do repositório, porque é quem a usa. memoria importa domain (para o tipo Pedido), nunca usecase — a satisfação implícita da Lição 2 dispensa esse import. main.go é o único ponto que conhece as três peças, e é lá que a Injeção de Dependência da Lição 1 acontece fisicamente: criar a implementação real e passá-la como parâmetro. Trocar memoria por qualquer outra implementação — arquivo, Postgres, o que for — muda uma linha em main.go e nada mais, porque domain e usecase nunca souberam qual implementação estavam usando.
Próxima aula planejada: Screaming Architecture — o que a árvore de pastas deveria gritar, e o que fazer quando ela grita "Go" ou "MVC" em vez do domínio do negócio.
Referências primárias para aprofundar
As camadas usadas nesta lição vêm de The Clean Architecture, Robert C. Martin (2012). A tradução prática desses conceitos para um projeto Go real segue How to implement Clean Architecture in Go, Three Dots Labs. A estrutura de pastas e o diretório internal estão documentados em Go Modules: Package and Module Layout, e o mesmo layout domain/adapter é descrito de forma independente em Standard Package Layout, Ben Johnson.
Consulte também o glossário desta trilha.