Aula 05 · 50–70 minutos

Screaming Architecture: o que a árvore de pastas deveria gritar

Aula 5 — por que a estrutura de pastas de um projeto deveria comunicar o negócio, não o framework; aplicando a lente ao aprovador-pedidos e ao anti-padrão do pacote models genérico.

Resultado e mapa da aula

Ao final desta aula você vai conseguir olhar para a estrutura de pastas de qualquer projeto Go — o aprovador-pedidos que você mesmo escreveu, ou um repositório de mercado que te derem para revisar numa entrevista — e responder uma pergunta específica: essa estrutura grita o negócio que o sistema resolve, ou grita a ferramenta usada para construí-lo? Isso não é uma questão de gosto estético. É um diagnóstico com consequência prática, e a Lição 1 já te deu o vocabulário para entendê-la: a Regra de Dependência.

  1. O problema: um projeto Go real, organizado do jeito que a maioria dos tutoriais ensina, que não revela o que faz.
  2. A fonte primária: Screaming Architecture, de Robert C. Martin — a analogia da planta de um prédio e três citações que definem o argumento.
  3. O que "gritar" quer dizer, com precisão — duas afirmações distintas, não uma só.
  4. Aplicando a lente ao aprovador-pedidos: por que ele já passa no teste, e onde exatamente o grito acontece.
  5. Duas escolas de organização de pacotes — por camada (o que este projeto usa) e por funcionalidade — comparadas com trade-offs, não com uma resposta única.
  6. O anti-padrão de mercado mais comum: o pacote models genérico, e por que ele viola duas regras ao mesmo tempo.

O problema: um projeto que grita "Gin", não "aprovação de pedidos"

Imagine que você entra num time novo e recebe acesso a um repositório Go que ninguém te explicou ainda. Antes de ler uma linha de lógica de negócio, você abre a árvore de pastas — é o primeiro contato que qualquer pessoa tem com um sistema. É comum encontrar algo assim, porque é exatamente o que a maioria dos tutoriais de API em Go ensina:

pedidos-api/
  cmd/
    api/
      main.go
  internal/
    handlers/
      order_handler.go
    models/
      order.go
    routes/
      routes.go
    middleware/
      auth.go
    db/
      postgres.go

Repare no que essa árvore de pastas comunica: handlers, models, routes, middleware são todos nomes genéricos de peças de um framework web — não dizem nada sobre o negócio. Abrindo order_handler.go, você normalmente encontra algo como:

// internal/handlers/order_handler.go
package handlers

import "github.com/gin-gonic/gin"

func CreateOrder(c *gin.Context) {
    var order models.Order
    if err := c.ShouldBindJSON(&order); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    // regra de negócio de aprovação, misturada aqui dentro
    if order.Total <= 1000 {
        order.Approved = true
    }
    db.Save(&order)
    c.JSON(200, order)
}

A regra de negócio real — "pedidos até um certo valor são aprovados automaticamente", a mesma regra da Lição 4 — está enterrada dentro de uma função que também faz parsing de JSON, validação de request, resposta HTTP e persistência, tudo amarrado ao pacote gin. Para descobrir o que esse sistema faz, você não lê a estrutura de pastas: você precisa abrir cada handler e desembaraçar a lógica de negócio dos detalhes de HTTP. Isso tem um custo mensurável: onboarding mais lento, revisão de código mais difícil (um PR que muda regra de negócio parece idêntico, no diff, a um PR que só ajusta validação de request), e testes de unidade da regra de aprovação que só rodam se você conseguir simular um *gin.Context — o oposto do que a Lição 4 demonstrou ser possível com um teste de use case puro.

Fonte primária: Screaming Architecture, Robert C. Martin (2011)

O argumento não para na analogia. Martin encadeia três afirmações que se apoiam:

O que "gritar" quer dizer, com precisão

É fácil ler "a estrutura deveria gritar o negócio" e concluir que toda pasta, em todo nível, precisa ter um nome específico de caso de uso. Isso é uma leitura exagerada. O argumento de Martin, lido com cuidado, tem duas partes distintas — e só uma delas é obrigatória:

  1. Os nomes não deveriam citar uma tecnologia específica. Uma pasta chamada gin/, gorm/ ou routes-do-gin/ grita a ferramenta. Isso é sempre um problema, em qualquer escala de projeto.
  2. Em algum nível visível — módulo, pacote ou arquivo — o negócio deveria aparecer. Isso não exige que cada pasta tenha o nome exato de um caso de uso. Exige que, olhando para o projeto, alguém consiga identificar do que ele trata, em algum ponto da árvore, sem precisar ler lógica interna de uma função.

A primeira parte é uma regra rígida: nunca nomeie um pacote de negócio pelo framework. A segunda é uma questão de grau, e é aqui que times de mercado divergem — a próxima seção mostra duas formas válidas de satisfazer essa segunda parte, com trade-offs diferentes, não uma "forma certa" única.

Aplicando a lente ao aprovador-pedidos

Volte à estrutura que a Lição 4 deixou pronta:

aprovador-pedidos/
  go.mod
  main.go
  internal/
    domain/
      pedido.go
    usecase/
      aprovar_pedido.go
    memoria/
      pedido_repository.go

Aplique as duas afirmações da seção anterior:

  • Nenhum nome cita uma tecnologia. domain, usecase, memoria são papéis arquiteturais — os mesmos nomes das quatro camadas da Lição 1 — não nomes de bibliotecas. Se você trocasse a implementação em memória por Postgres (o que a Lição 11 faz), o pacote se chamaria postgres, não sql ou gorm; ainda um papel, não uma marca. net/http só vai aparecer dentro de um adapter, na Lição 15 — nunca no nome de um pacote de domínio ou use case.
  • O negócio aparece — mas não em todo nível. O go.mod do projeto declara module github.com/allangrds/aprovador-pedidos: o nome do módulo, a primeira coisa que qualquer pessoa vê ao clonar o repositório, já grita "isto é sobre aprovar pedidos". Dentro de internal/usecase/, o arquivo se chama aprovar_pedido.go, não usecase1.go ou handler.go — o caso de uso específico aparece no nome do arquivo. O que não grita o caso de uso específico é o nome do pacote usecase em si — ele é um papel genérico ("aqui moram os casos de uso"), não "aprovação-de-pedidos".
Estrutura que grita o framework vs. estrutura que grita o domínioUm projeto Go organizado por handlers, models com tags gorm e routes grita "Gin" — o framework usado. O projeto aprovador-pedidos, organizado por domain, usecase e memoria, dentro de um módulo chamado aprovador-pedidos, grita o papel arquitetural de cada pacote e o negócio no nome do módulo — nenhum nome cita um framework.Grita o frameworknomes técnicos, Gin aparece em quase todo lugarcmd/api/main.go — router := gin.Default()internal/handlers/order_handler.go — usa gin.Contextinternal/models/order.go — tags gorm:"column:..."internal/routes/routes.go — gin.RouterGroupPergunta: o que este sistema faz?Resposta possível só depois de abrirhandlers e ler a lógica dentro deles.Grita o domínionenhum pacote cita um frameworkmodule aprovador-pedidosgo.mod — o negócio já aparece aquiinternal/domain/pedido.go — regra purainternal/usecase/aprovar_pedido.go — caso de usointernal/memoria/pedido_repository.go — adapterPergunta: o que este sistema faz?Resposta: aprova pedidos — dá pra lerno nome do módulo, antes de abrir um arquivo.

À esquerda, um projeto organizado por partes de um framework web: handlers, models com tags de ORM, routes — a estrutura grita "Gin", e só revela o negócio depois de abrir um arquivo. À direita, aprovador-pedidos: o nome do módulo já grita o negócio, e os pacotes internos usam nomes de papel arquitetural — nenhum cita uma ferramenta específica.

Essa distinção — negócio visível no módulo e nos arquivos, papéis arquiteturais visíveis nos pacotes — é uma escolha de projeto, não a única forma correta de aplicar Screaming Architecture. A próxima seção nomeia essa escolha e a compara com a alternativa mais citada no mercado.

Duas escolas de organização: por camada vs. por funcionalidade

O aprovador-pedidos usa package by layer (organização por camada): cada pacote representa um papel arquitetural (domain, usecase, memoria), e cresce horizontalmente conforme o sistema ganha mais casos de uso — a Lição 6 vai adicionar um segundo arquivo dentro de usecase/ (rejeitar_pedido.go), não um pacote novo. A alternativa, citada com frequência em discussões de arquitetura Go, é package by feature (organização por funcionalidade): cada caso de uso ganha seu próprio pacote, contendo tudo que ele precisa.

# Package by layer (o que este projeto usa)
internal/
  domain/
    pedido.go
  usecase/
    aprovar_pedido.go
    rejeitar_pedido.go       # entra na Lição 6
  memoria/
    pedido_repository.go

# Package by feature (a alternativa)
internal/
  aprovarpedido/
    pedido.go                # domínio, só o que esta feature usa
    usecase.go
    repository_memoria.go
  rejeitarpedido/
    pedido.go                # possível duplicação do tipo Pedido
    usecase.go
    repository_memoria.go

Nenhuma das duas viola Screaming Architecture — as duas evitam nomear pacotes por framework, e as duas comunicam o negócio (uma no nome do pacote, a outra no nome do arquivo dentro de um pacote de papel). A escolha entre elas depende de escala, não de "qual é mais correta":

CritérioPackage by layerPackage by feature
Projeto pequeno, poucos casos de uso, um time sóSimples de navegar; poucos pacotesOverhead cedo demais — cada feature de 2 arquivos vira uma pasta
Regras de domínio compartilhadas entre casos de usoNatural — domain.Pedido é um tipo só, usado por todosRisco de duplicar Pedido em cada feature, ou criar um pacote shared que vai virando um segundo domain disfarçado
Muitos times, cada um dono de um pedaço vertical do sistemaCada mudança de feature toca vários pacotes (usecase, depois postgres, depois http) — mais coordenação entre timesCada time mexe majoritariamente dentro da própria pasta de feature — menos conflito de merge entre times
Candidato a virar módulo/serviço separado no futuroFronteiras de camada não coincidem com fronteiras de extraçãoFronteira de feature já é candidata natural a virar um módulo — tema retomado na Lição 30

Este curso continua com package by layer pelo resto da trilha — é a decisão de escopo já reaberta e confirmada, não uma omissão. As lições seguintes assumem que domain e usecase continuam pacotes únicos e crescentes (a Lição 8 adiciona um segundo caso de uso dentro do mesmo usecase/, por exemplo), e a essa escala — um projeto de aprendizado, um único desenvolvedor, um punhado de casos de uso — package by layer mantém a navegação simples sem o overhead de várias pastas de feature quase vazias. A Lição 30 volta a este tema quando o projeto tiver pacotes suficientes para avaliar, com critério concreto, se algum deles amadureceu o bastante para virar um módulo — momento em que fronteiras de feature (ou de pacote) importam de verdade.

Uma abordagem plausível, mas incorreta: o pacote models genérico

Por que essa abordagem parece razoável: ela evita duplicar a definição de Pedido — um tipo só, usado pelo handler HTTP (via json), pelo banco (via gorm) e por qualquer lógica de negócio que precise do campo Total. É um instinto compreensível: DRY (não se repita) é um princípio real, e duplicar um struct em três lugares parece desperdício.

Por que falha: essa abordagem viola duas regras ao mesmo tempo, não uma só.

  1. Screaming Architecture: o nome models não cita um framework diretamente, mas as tags dentro do struct citam — gorm:"primaryKey" amarra esse tipo à convenção de mapeamento do GORM. Qualquer pessoa que olhar esse arquivo aprende sobre GORM antes de aprender sobre a regra de negócio "quando um pedido é aprovado". O pacote grita "isto usa GORM", mesmo com um nome genérico.
  2. A Regra de Dependência (Lição 1): o tipo que representa a regra de negócio agora depende de uma biblioteca de infraestrutura (GORM) só para existir — mesmo que nenhuma linha de lógica de negócio use essa dependência diretamente, o tipo carrega a marca dela. Compare com domain.Pedido da Lição 4: zero imports externos, testável sem nada além da biblioteca padrão. models.Pedido com tags gorm não pode mais alegar isso — o compilador não recusa, mas o acoplamento existe na forma dos metadados da struct.

Consequência em produção: se o time decidir trocar GORM por pgx puro (uma migração real e comum, como a Lição 11 vai mostrar ao escolher pgx para este curso), toda referência a models.Pedido — em handlers, em testes, em qualquer lugar que o tipo apareça — precisa ser revisitada, porque o mesmo struct que carrega a regra de negócio também carrega o contrato de persistência. Num projeto pequeno isso é irritante; num projeto com dezenas de handlers usando models.Pedido, é um refactor arriscado que mistura mudança de negócio com mudança de infraestrutura no mesmo diff — exatamente o tipo de PR difícil de revisar que a introdução desta aula descreveu.

A correção, aplicando o que a Lição 4 já fez sem nomear explicitamente: mantenha domain.Pedido livre de qualquer tag de persistência, e crie um tipo específico do adapter — como o registro que o exercício da Lição 4 introduziu dentro de internal/arquivo — para carregar as tags que uma tecnologia de infraestrutura específica exige.

// internal/domain/pedido.go — sem tags, sem imports externos
package domain

type Pedido struct {
    ID    string
    Total float64
}

// internal/postgres/pedido_repository.go — tags isoladas aqui
package postgres

type linhaPedido struct {
    ID       string  `db:"id"`
    Total    float64 `db:"total"`
    Aprovado bool    `db:"aprovado"`
}

domain.Pedido nunca sabe que Postgres existe. postgres.linhaPedido é responsabilidade exclusiva do adapter, convertida de e para domain.Pedido dentro do próprio pacote postgres — o mesmo raciocínio de DTO que a Lição 9 nomeia formalmente mais adiante no curso.

Prática guiada: reorganize um repositório que grita o framework

Raciocínio antes do código

O diagnóstico pede para separar dois tipos de sintoma: nomes de pasta que citam tecnologia diretamente (handlers é genérico, mas o conteúdo deles não é), e regra de negócio fisicamente presa a uma dependência externa (o *gin.Context dentro do handler, as tags gorm dentro do model). A correção não é só "renomear pastas" — Martin é explícito que arquitetura não é sobre nomenclatura cosmética, é sobre onde a lógica de negócio pode rodar sem a ferramenta presente.

Uma abordagem plausível, mas incorreta: só renomear as pastas

biblioteca-api/
  main.go
  entities/          # "models" virou "entities" — só o nome mudou
    livro.go          # struct ainda tem tags gorm
    emprestimo.go
  usecases/           # "handlers" virou "usecases" — só o nome mudou
    livro_usecase.go  # ainda recebe *gin.Context como parâmetro
    emprestimo_usecase.go

Por que falha: os nomes agora parecem os certos — entities, usecases até ecoam o vocabulário da Lição 1 — mas nada mudou na prática. livro.go ainda importa gorm; livro_usecase.go ainda recebe *gin.Context como parâmetro, então ainda é impossível testar a regra "não permitir empréstimo se já houver cópia emprestada" sem simular o Gin inteiro. Isso é o erro mais comum ao aplicar Screaming Architecture superficialmente: tratá-la como um problema de nomenclatura, quando o teste real — da citação de Martin sobre testabilidade — é se a regra de negócio roda sem o framework, não se a pasta tem um nome bonito.

A solução correta: mover a dependência, não só o nome

biblioteca-api/
  main.go                        # cria gin.Engine, injeta as implementações
  internal/
    domain/
      livro.go                    # struct Livro, sem tags
      regra_emprestimo.go         # a regra "não emprestar se já emprestado", pura
    usecase/
      emprestar_livro.go          # orquestra domain, declara a interface do repositório
    memoria/
      livro_repository.go         # implementação, sem conhecer usecase
    http/
      livro_handler.go            # só parsing de JSON + chamada ao usecase — Gin fica só aqui

Por que está correto: a regra de negócio (regra_emprestimo.go) agora vive dentro de domain, sem nenhum import de gin ou gorm — pode ser testada exatamente como pedido_test.go foi testado na Lição 4, sem simular nada. gin.Context só aparece dentro de internal/http, a única camada que deveria conhecê-lo — a mesma separação que main.go demonstrou na Lição 4 entre a camada que faz o "wiring" e as camadas que não sabem qual implementação estão usando. O nome do módulo (module biblioteca-api no go.mod) já responde à pergunta "o que este sistema faz" antes mesmo de abrir internal/ — é a mesma função que module aprovador-pedidos cumpre neste curso: o negócio aparece no nível mais visível de todos, o nome do repositório.

Verificação: depois de mover a regra para domain, escreva um teste de unidade para regra_emprestimo.go que não importa gin nem gorm — se o teste compila e roda sem esses imports na lista de dependências (confira com go list -deps ./internal/domain, a mesma técnica de verificação que a Lição 4 usou), a correção está completa.

Perguntas de entrevista e de mercado

"O que é Screaming Architecture, na sua própria definição — não a definição decorada do artigo?" Uma boa resposta separa as duas afirmações que esta aula distinguiu: (1) nomes de pastas e pacotes nunca deveriam citar uma tecnologia específica, e (2) em algum nível da estrutura — módulo, pacote ou arquivo — o negócio deveria ser identificável sem precisar ler lógica interna. Evite responder só com a analogia da planta do prédio sem explicar o motivo prático por trás dela: testabilidade e independência de framework, não estética.

"Você já revisou um projeto Go real organizado com um pacote models genérico contendo tags de JSON e de ORM juntas. Isso é um problema de Screaming Architecture, de Regra de Dependência, os dois, ou nenhum?" A resposta forte é "os dois, e eles se reforçam": o pacote em si pode ter um nome inofensivo (models não cita GORM), mas as tags dentro do struct amarram a definição do negócio a uma biblioteca de infraestrutura — isso é uma violação da Regra de Dependência (Lição 1) que se manifesta também como uma violação de Screaming Architecture, porque quem abre esse arquivo aprende sobre a convenção do ORM antes de aprender sobre o negócio. Cite o exemplo concreto do linhaPedido desta aula como a correção.

"Screaming Architecture exige que cada caso de uso tenha seu próprio pacote? O projeto aprovador-pedidos, com pacotes domain/usecase/memoria compartilhados entre vários casos de uso, contraria o princípio?" Não — esta é uma pergunta que testa se o candidato leu o artigo original com cuidado ou só decorou a versão simplificada. Explique a distinção entre package-by-layer e package-by-feature (ambas válidas) e o critério de escolha real: escala do projeto e número de times, não conformidade obrigatória com uma estrutura específica. Cite que mesmo o código de exemplo do livro Clean Architecture usa majoritariamente organização por camada.

"Como você aplicaria esse teste (a árvore de pastas grita o quê?) ao avaliar um repositório desconhecido numa entrevista técnica ou num code review?" Descreva um processo concreto, não uma opinião vaga: primeiro, olhe o nome do módulo/repositório — ele identifica o negócio? Segundo, liste os pacotes de nível mais alto — algum nome cita uma tecnologia (gin, gorm, redis) diretamente? Terceiro, escolha uma regra de negócio central e pergunte se ela pode ser testada sem subir nenhuma infraestrutura — é o teste prático da citação de Martin sobre testabilidade, o critério mais objetivo dos três.

Recupere da memória: 10 questões

Recupere da memória: 10 questões

Segundo Robert C. Martin, o que a estrutura de pastas de nível mais alto de um sistema deveria comunicar?

No exemplo do handler Gin desta aula, por que a regra de negócio 'pedidos até um certo valor são aprovados automaticamente' não pode ser testada sem o framework?

Segundo a citação de Martin sobre testabilidade, o que uma boa arquitetura deveria permitir?

Screaming Architecture exige que cada pasta, em todo nível da árvore, tenha o nome exato de um caso de uso específico?

No projeto aprovador-pedidos, onde exatamente o negócio 'aparece' de forma mais visível, já no primeiro contato com o repositório?

Os nomes de pacote domain, usecase e memoria, usados neste projeto, citam alguma tecnologia específica?

Qual é a principal vantagem de package-by-feature sobre package-by-layer, segundo a comparação desta aula?

Por que este curso continua usando package-by-layer pelo resto da trilha, em vez de migrar para package-by-feature?

No anti-padrão do pacote models genérico desta aula, por que a struct Pedido com tags gorm viola a Regra de Dependência da Lição 1, mesmo que o código compile normalmente?

Na solução correta do anti-padrão models, onde as tags de persistência (como db:"id") deveriam ficar?

Feche o modelo mental

Screaming Architecture não é sobre nomear pastas de forma bonita — é sobre onde a regra de negócio pode rodar. Um projeto grita o framework quando a lógica central está fisicamente presa a um tipo de biblioteca externa (*gin.Context, tags gorm), mesmo que os nomes de pasta pareçam neutros. O aprovador-pedidos já passa nesse teste hoje: o nome do módulo grita o negócio, nenhum pacote interno cita uma tecnologia, e a regra de aprovação (Lição 4) já rodava sem nenhum framework presente. Package-by-layer (o que este projeto usa) e package-by-feature são duas formas igualmente válidas de satisfazer a segunda parte do argumento — a escolha depende de escala, não de correção absoluta. O anti-padrão mais perigoso não é um nome de pasta ruim: é um struct de domínio carregando tags de uma biblioteca de infraestrutura, um acoplamento invisível ao compilador, mas real na hora de trocar essa infraestrutura.

Próxima aula planejada: Um terceiro use case — rejeitar um pedido, e o estado que faltava no domínio até agora.

Referências primárias para aprofundar

O argumento central desta aula vem de Screaming Architecture, Robert C. Martin (Clean Coder Blog, 2011), publicado um ano antes de "The Clean Architecture", a fonte primária da Lição 1. A comparação entre organização por camada e por funcionalidade é discutida de forma tecnologicamente agnóstica em Fundamentals of Software Architecture, Mark Richards & Neal Ford. A raiz do anti-padrão do pacote models genérico — misturar a definição do domínio com detalhes de persistência — é o mesmo problema que Standard Package Layout, Ben Johnson (já citado na Lição 4) resolve isolando cada dependência externa em seu próprio subpacote.

Consulte também o glossário desta trilha.