Clean Architecture em Go costuma aparecer em dois extremos. De um lado, projetos onde tudo fica em main.go, handlers acessam banco diretamente, regra de negócio se mistura com JSON e qualquer teste exige subir metade da aplicação. Do outro, repositórios com cinco camadas, factories, providers, DTOs duplicados e interfaces para tudo antes mesmo de existir uma regra de negócio real.
Go não combina bem com arquitetura teatral. A linguagem favorece código explícito, pacotes pequenos, interfaces no ponto de uso e dependências fáceis de enxergar. Isso não significa abandonar arquitetura. Significa usar Clean Architecture como ferramenta para proteger regras importantes, não como ritual para deixar o projeto com cara de enterprise.
Este guia mostra quando Clean Architecture faz sentido em Go, quando ela vira overengineering e como aplicar uma versão pragmática em APIs reais. Se você quer um tutorial completo com estrutura de pastas, exemplos longos e testes, veja também Go Clean Architecture. Para a base de estilo, leia Effective Go em 2026. Para APIs, combine com o guia de API REST em Go.
O problema que a arquitetura precisa resolver
Arquitetura não serve para impressionar entrevista. Ela serve para reduzir custo de mudança. Antes de criar camadas, pergunte qual problema concreto está doendo.
Em uma API Go comum, os problemas aparecem assim:
- Handler HTTP validando JSON, consultando banco, calculando regra e montando resposta no mesmo bloco.
- Regra de negócio duplicada entre endpoint público, job assíncrono e comando administrativo.
- Teste que só roda com PostgreSQL, Redis, variáveis de ambiente e servidor HTTP de pé.
- Troca de gateway de pagamento exigindo alteração em vários handlers.
- Pacotes importando uns aos outros até aparecer
import cycle not allowed. - Deploy arriscado porque ninguém sabe onde a regra realmente vive.
Clean Architecture ajuda quando separa decisões de negócio dos detalhes externos: HTTP, SQL, fila, cache, provedor de e-mail, clock, logger e configuração. O ponto não é ter nomes bonitos como domain, application e infrastructure. O ponto é conseguir testar a regra sem depender de detalhe externo e trocar detalhe externo sem reescrever a regra.
Quando Clean Architecture faz sentido
Use uma separação mais explícita quando o domínio tem regras que merecem proteção. Exemplos:
- Produtos financeiros, cobrança, assinatura, crédito ou antifraude.
- Workflows com estados, aprovações e transições importantes.
- APIs usadas por múltiplos clientes: web, mobile, parceiros e jobs internos.
- Sistemas com fila, eventos, webhooks ou processamento assíncrono.
- Times onde várias pessoas mexem no mesmo backend.
- Serviços que devem sobreviver a troca de banco, broker, provedor externo ou framework HTTP.
Nesses casos, misturar tudo no handler cobra juros. Um endpoint de criação de pedido pode começar simples, mas logo ganha validação de estoque, cupom, antifraude, idempotência, cobrança, envio de evento e notificação. Se tudo está acoplado ao http.ResponseWriter, cada mudança vira cirurgia.
Uma versão pragmática de Clean Architecture cria um centro mais estável para a aplicação. O handler traduz HTTP para comando. O caso de uso executa a regra. Repositories e gateways escondem detalhes externos. O banco continua importante, mas deixa de ser o lugar onde toda decisão nasce.
Quando vira overengineering
Clean Architecture vira problema quando a estrutura cresce antes da necessidade. Em Go, sinais comuns de exagero são:
- Interface criada no mesmo pacote da implementação sem haver consumidor alternativo.
UserController,UserService,UserUseCase,UserInteractor,UserManagereUserHandlerfazendo quase a mesma coisa.- DTO diferente para cada camada, com campos idênticos e conversões mecânicas.
- Pacote
domainanêmico, só com structs sem comportamento. repositorygenérico demais, tentando abstrair SQL como se banco fosse detalhe irrelevante.- Testes cheios de mocks frágeis que repetem a implementação.
- Pastas copiadas de outro ecossistema sem conversar com idiomatismo Go.
Se a aplicação é um CRUD pequeno, interno, com duas tabelas e vida curta, comece simples. Um pacote com handlers, um pacote de storage e testes claros pode ser suficiente. Arquitetura boa também sabe não aparecer.
O critério prático é: a separação deve remover dor, não adicionar cerimônia. Se uma camada não protege uma regra, não facilita teste e não reduz acoplamento real, talvez ela seja só decoração.
Uma estrutura mínima e honesta
Para uma API Go média, uma estrutura pequena costuma bastar:
cmd/api/main.go
internal/order/
handler.go
service.go
store.go
model.go
internal/platform/postgres/
order_store.go
Não existe obrigação de criar domain, application, usecase, adapter, driver e infrastructure no primeiro commit. Você pode organizar por feature e manter a regra perto do vocabulário do domínio.
Um service.go pode representar o caso de uso:
package order
import "context"
type Store interface {
Save(ctx context.Context, order Order) error
ByID(ctx context.Context, id string) (Order, error)
}
type PaymentGateway interface {
Charge(ctx context.Context, payment Payment) (ChargeResult, error)
}
type Service struct {
store Store
payments PaymentGateway
}
func NewService(store Store, payments PaymentGateway) *Service {
return &Service{store: store, payments: payments}
}
func (s *Service) Create(ctx context.Context, cmd CreateOrder) (Order, error) {
order, err := NewOrder(cmd.CustomerID, cmd.Items)
if err != nil {
return Order{}, err
}
charge, err := s.payments.Charge(ctx, order.Payment())
if err != nil {
return Order{}, err
}
order.MarkPaid(charge.ID)
if err := s.store.Save(ctx, order); err != nil {
return Order{}, err
}
return order, nil
}
Repare que as interfaces ficam no pacote que consome a dependência. Isso é idiomático em Go. O serviço não precisa saber se Store é PostgreSQL, SQLite, memória, arquivo ou mock de teste. Ele precisa saber apenas o contrato necessário para cumprir o caso de uso.
Handler não deve ser o dono da regra
O handler HTTP deve fazer trabalho de borda:
- Ler request.
- Decodificar JSON.
- Validar formato básico.
- Chamar o caso de uso.
- Traduzir erro para status HTTP.
- Escrever resposta.
Ele não deve decidir política de cobrança, regra de estoque ou transição de estado. Um handler mais limpo fica assim:
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req createOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "json inválido")
return
}
order, err := h.service.Create(r.Context(), CreateOrder{
CustomerID: req.CustomerID,
Items: req.Items,
})
if err != nil {
writeDomainError(w, err)
return
}
writeJSON(w, http.StatusCreated, orderResponseFrom(order))
}
Esse desenho permite chamar o mesmo Service.Create a partir de um worker Kafka, uma rotina administrativa, um CLI interno ou um teste de unidade. HTTP vira uma porta de entrada, não o coração da aplicação.
Interfaces pequenas vencem abstrações grandes
Um erro comum é criar uma interface gigante porque a implementação concreta tem muitos métodos:
type Repository interface {
CreateUser(ctx context.Context, user User) error
UpdateUser(ctx context.Context, user User) error
DeleteUser(ctx context.Context, id string) error
ListUsers(ctx context.Context) ([]User, error)
CreateOrder(ctx context.Context, order Order) error
ListOrders(ctx context.Context, userID string) ([]Order, error)
}
Isso acopla casos de uso que não precisam se conhecer. Em Go, prefira interfaces pequenas, criadas perto do uso:
type OrderStore interface {
Save(ctx context.Context, order Order) error
}
Essa escolha facilita teste e reduz impacto de mudança. Se amanhã ListOrders muda por causa de paginação, o caso de uso que só salva pedido não precisa saber.
Banco de dados é detalhe, mas não finja que ele não existe
Clean Architecture às vezes é interpretada como “o banco não importa”. Isso é perigoso. Banco importa muito: transações, índices, constraints, isolamento, locks e performance moldam o comportamento real do sistema.
O ponto é impedir que cada handler escreva SQL de qualquer jeito. Uma implementação PostgreSQL pode ficar em pacote externo ao domínio, mas ainda precisa respeitar transações, erros esperados e constraints. Em regras críticas, a constraint do banco é parte da segurança.
Exemplo: idempotência, saldo, unicidade de e-mail e estado de pedido não devem depender apenas de if em memória. Use o banco para proteger invariantes concorrentes e exponha isso por um método claro. Para evoluir schema sem susto, leia também migrations em Go.
Teste a regra sem subir o mundo
O ganho mais visível da arquitetura pragmática é teste. Um caso de uso com interfaces pequenas permite testar regra de negócio com fakes simples:
type fakeStore struct {
saved Order
}
func (f *fakeStore) Save(ctx context.Context, order Order) error {
f.saved = order
return nil
}
Você não precisa mockar cada detalhe com framework pesado. Muitas vezes, um fake pequeno escrito no próprio teste é mais claro. O teste deve dizer: dado este comando, esta regra produz este resultado e chama este limite externo.
Ainda assim, não abandone testes de integração. Repository PostgreSQL, migrations, transações e queries importantes merecem teste com banco real. A divisão saudável é:
- Regras de negócio: testes rápidos com fakes.
- SQL e adapters: testes de integração focados.
- Fluxo HTTP principal: poucos testes end-to-end para garantir wiring.
Como evoluir sem reescrever tudo
Você não precisa começar perfeito. Um caminho seguro:
- Comece com handler e store simples.
- Quando uma regra crescer ou for reutilizada, extraia um service/case de uso.
- Crie interface apenas para dependência que o caso de uso realmente consome.
- Mova detalhes externos para implementação concreta.
- Adicione testes no nível onde a decisão acontece.
- Só crie nova camada quando houver acoplamento real para cortar.
Essa evolução combina melhor com Go do que tentar prever todas as abstrações no primeiro dia. O código nasce simples e ganha estrutura onde a pressão aparece.
Clean Architecture e vagas Go no Brasil
Muitas vagas Go no Brasil citam APIs REST, microserviços, Clean Architecture, SOLID, mensageria, Docker, Kubernetes, observabilidade e bancos SQL/NoSQL. Em entrevista, porém, o diferencial não é repetir nomes de padrões. É explicar trade-offs.
Uma boa resposta soa assim: “eu deixo handler fino, coloco regra em caso de uso testável, defino interfaces pequenas no consumidor, mantenho SQL em adapter com testes de integração e evito camada que só repassa chamada”. Isso mostra maturidade. Você entende a arquitetura, mas não virou refém dela.
Para portfólio, vale mais publicar uma API pequena com README explicando decisões do que um template enorme sem caso real. Mostre endpoint, migration, teste, Docker, logs e uma regra de negócio que justifique separação.
Checklist prático
Antes de dizer que seu projeto precisa de Clean Architecture, revise:
- Existe regra de negócio que precisa ser testada fora de HTTP?
- O mesmo caso de uso será chamado por API, worker, CLI ou evento?
- Há dependências externas que podem mudar: banco, fila, gateway, storage?
- Interfaces foram criadas no pacote consumidor?
- Cada interface tem poucos métodos e motivo claro?
- O handler está fino ou concentra regra?
- O banco protege invariantes importantes com constraints/transações?
- Os testes cobrem regra, adapter e wiring em níveis diferentes?
- Alguma camada só repassa chamada sem adicionar valor?
- O nome dos pacotes comunica domínio, não organograma abstrato?
Conclusão
Clean Architecture em Go é útil quando protege regras, facilita testes e reduz acoplamento com detalhes externos. Ela vira overengineering quando troca simplicidade por cerimônia e cria camadas que não pagam aluguel.
O caminho mais forte para Go em produção é pragmático: handlers finos, casos de uso claros, interfaces pequenas, adapters concretos, banco levando consistência a sério e testes no nível certo. Assim você ganha a parte boa da arquitetura sem abandonar o que faz Go funcionar bem: clareza, composição e manutenção simples.