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.
- O problema: um projeto Go real, organizado do jeito que a maioria dos tutoriais ensina, que não revela o que faz.
- 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.
- O que "gritar" quer dizer, com precisão — duas afirmações distintas, não uma só.
- Aplicando a lente ao aprovador-pedidos: por que ele já passa no teste, e onde exatamente o grito acontece.
- 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.
- O anti-padrão de mercado mais comum: o pacote
modelsgené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:
- Os nomes não deveriam citar uma tecnologia específica. Uma pasta chamada
gin/,gorm/ouroutes-do-gin/grita a ferramenta. Isso é sempre um problema, em qualquer escala de projeto. - 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,memoriasã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 chamariapostgres, nãosqlougorm; ainda um papel, não uma marca.net/httpsó 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.moddo projeto declaramodule 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 deinternal/usecase/, o arquivo se chamaaprovar_pedido.go, nãousecase1.goouhandler.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 pacoteusecaseem si — ele é um papel genérico ("aqui moram os casos de uso"), não "aprovação-de-pedidos".
À 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ério | Package by layer | Package by feature |
|---|---|---|
| Projeto pequeno, poucos casos de uso, um time só | Simples de navegar; poucos pacotes | Overhead cedo demais — cada feature de 2 arquivos vira uma pasta |
| Regras de domínio compartilhadas entre casos de uso | Natural — domain.Pedido é um tipo só, usado por todos | Risco 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 sistema | Cada mudança de feature toca vários pacotes (usecase, depois postgres, depois http) — mais coordenação entre times | Cada 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 futuro | Fronteiras de camada não coincidem com fronteiras de extração | Fronteira 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ó.
- Screaming Architecture: o nome
modelsnã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. - 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.Pedidoda Lição 4: zero imports externos, testável sem nada além da biblioteca padrão.models.Pedidocom tagsgormnã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.