Voltar ao blog
9 de maio de 20267 min de leitura

Clean Architecture em Go na Prática - Guia Completo de Separação de Camadas com Projeto Real!

gocleanarchitecturebackendprogramming

Já abriu um projeto de 6 meses atrás e não conseguiu entender onde ficava a lógica de negócio? Eu já. Controller com 200 linhas fazendo query no banco, validação, cálculo e formatação de resposta tudo junto. Qualquer mudança quebrava 3 coisas que não tinham relação aparente. O problema não era o código em si, era a falta de separação clara entre o que é regra de negócio e o que é detalhe técnico. Este artigo mostra como resolvi isso num projeto real de crypto tracker usando Clean Architecture em Go, de forma pragmática e sem over-engineering.

Guia de tópicos:

  • A Estrutura de Pastas e Por Que Cada Pasta Existe
  • Entities: Seu Domínio Não Pode Depender de Framework
  • Interfaces como Contratos: A Peça que Conecta Tudo
  • A Camada de Apresentação: Onde o HTTP Mora
  • Use Cases: Onde a Lógica de Negócio Realmente Vive
  • Testabilidade: O Benefício que Paga Todo o Esforço
  • Quando NÃO Usar Clean Architecture
  • Considerações Finais

A Estrutura de Pastas e Por Que Cada Pasta Existe

Antes de qualquer linha de código, a decisão mais importante que você vai tomar é onde cada coisa vai morar. Eu sei que parece bobeira, mas a verdade é que a estrutura de pastas é o primeiro sinal de que um projeto vai ser fácil ou difícil de manter. Quando você olha para um projeto e em 5 segundos entende onde está a lógica de negócio, onde estão os handlers HTTP e onde estão as implementações de banco, isso é um projeto bem organizado.

No projeto que vou usar como referência, a estrutura ficou assim: temos uma pasta cmd/ que são os pontos de entrada da aplicação (o servidor HTTP e a CLI de migrations), uma pasta internal/domain/ que é o coração de tudo (entidades, regras de negócio e interfaces), uma pasta internal/infra/ que são as implementações concretas (PostgreSQL, InfluxDB, Firebase, Binance WebSocket) e uma pasta internal/presentation/ que é a casca HTTP (controllers, DTOs, middlewares).

A regra de ouro aqui é simples: as dependências sempre apontam para dentro. O domínio não importa nada de infraestrutura. O domínio não sabe que HTTP existe. O domínio define interfaces e as outras camadas implementam. Se amanhã você trocar o PostgreSQL por MongoDB, só a pasta de infra muda. O resto do sistema nem percebe.

E tem um detalhe que o Go te dá de graça: o pacote internal/ garante que nada fora do seu módulo consegue importar esses pacotes. É encapsulamento sem precisar de nenhum framework ou configuração extra.

Entities: Seu Domínio Não Pode Depender de Framework

Aqui é onde muita gente erra. A entity não é um "model do banco de dados". A entity é a representação mais pura do seu domínio. Ela não sabe que HTTP existe, não sabe que PostgreSQL existe, não sabe que JSON existe. Ela só conhece as regras do negócio.

Pensa assim: se você tem um User, a entity é quem define que um usuário precisa ter nome, que o email precisa ser válido, que não pode existir um usuário sem esses dados. Isso é regra de negócio. Não é validação de request HTTP, não é constraint de banco. É o seu domínio dizendo "olha, para existir um usuário no meu sistema, essas condições precisam ser verdadeiras".

No projeto, o User tem um construtor NewUser que valida nome e email antes de criar a instância. Se o email é inválido, o erro acontece ali, na entity. Não no controller, não no banco. A regra mora onde deveria morar.

Agora, nem toda entity precisa de um construtor elaborado. O Position por exemplo (que representa uma posição de investimento do usuário em uma criptomoeda) tem 11 campos, e a validação dele é mais complexa porque precisa verificar se a crypto existe no sistema, se o usuário é válido, calcular o valor investido. Isso tudo é responsabilidade do use case, não da entity. A entity só define a estrutura e as regras mais básicas de existência.

Uma decisão pragmática que tomei foi colocar tags db:"..." nas entities para o sqlx fazer scan automático. Puristas vão dizer que isso acopla a entity ao banco. Na prática, é só metadata, a entity não importa o sqlx, e evita criar uma camada inteira de mapeamento que não agrega valor real no projeto.

Interfaces como Contratos: A Peça que Conecta Tudo

Essa é a parte que faz a Clean Architecture realmente funcionar, e é onde muita gente se perde. O conceito é simples: você cria um pacote que define o que o domínio precisa, sem dizer como implementar. São interfaces. Contratos. Promessas.

No projeto, existe um pacote chamado repositorier/ dentro do domínio. Ele tem interfaces como PositionRepositorier que define: "eu preciso de alguém que saiba salvar uma posição, buscar por ID, buscar por usuário, atualizar e deletar". Quem implementa isso? Não importa para o domínio. Pode ser PostgreSQL, pode ser MongoDB, pode ser um arquivo JSON. O domínio não se importa.

E aqui vem o poder real dessa abordagem: no projeto, o PositionRepositorier é implementado pelo PostgreSQL, mas o CryptoHistoryRepository (que guarda histórico de preços) é implementado pelo InfluxDB. São bancos completamente diferentes, com paradigmas diferentes (relacional vs time-series), mas o use case que usa os dois não importa nenhum pacote de banco de dados. Ele trabalha com interfaces.

O PortfolioUseCase por exemplo usa 4 repositórios de 2 bancos diferentes para calcular o lucro/prejuízo do usuário. E ele não tem a menor ideia de que está falando com PostgreSQL e InfluxDB ao mesmo tempo. Se amanhã eu migrar o histórico de preços para TimescaleDB, o use case não muda uma linha. Só a implementação na pasta de infra muda.

A implementação concreta fica na pasta infra/. Lá você tem o PositionRepository que faz queries SQL com sqlx, e o CryptoHistoryRepositoryImpl que escreve pontos no InfluxDB. Ambos retornam a interface, não o struct concreto. Quem consome não precisa saber o tipo real.

A Camada de Apresentação: Onde o HTTP Mora

A presentation layer é a casca da aplicação. É ela que recebe o request HTTP, converte para o formato que o domínio entende, chama o use case e formata a resposta. Nada mais que isso.

Aqui entram três conceitos importantes: DTOs, Controllers e Middlewares.

O DTO (Data Transfer Object) é o contrato com o frontend. Ele define o que o cliente manda no body do request. E aqui tem um ponto crucial: o DTO não é a entity. O CreatePosition por exemplo tem 4 campos (crypto, quantidade, preço, data). A entity Position tem 11. O DTO não tem UserID porque isso vem do token de autenticação. Não tem InvestedAmount porque isso é calculado pelo use case. Não tem Status porque toda posição nova é "active". O DTO é enxuto, é só o que o frontend precisa mandar.

O Controller é a cola entre HTTP e use case. Ele faz exatamente 4 coisas: extrai o usuário do context (que o middleware colocou lá), decodifica o JSON de input, chama o use case, e formata a resposta. Nenhuma lógica de negócio. Se eu trocar Gorilla Mux por Chi ou pelo net/http padrão do Go, só o controller muda. O use case nem percebe.

O Middleware cuida dos cross-cutting concerns. No projeto, o middleware de autenticação valida o token Firebase, busca o usuário no banco e injeta o ID dele no context do request. Os controllers só chamam GetUserID(context) e pronto. Não sabem nada sobre Firebase, tokens ou validação. Isso é separação de responsabilidades na prática.

E tem o formato de resposta padronizado: todo endpoint retorna {"success": true, "data": ...} ou {"success": false, "error": "..."}. O frontend sabe que sempre vai ter esse formato. Sem surpresas, sem ficar checando "será que esse endpoint retorna diferente?".

Use Cases: Onde a Lógica de Negócio Realmente Vive

O use case é onde a mágica acontece. É o orquestrador. Ele recebe dados já validados do controller, aplica as regras de negócio, coordena os repositórios e retorna o resultado. Ele não sabe nada sobre HTTP, banco de dados ou frameworks.

Vou dar um exemplo concreto. O PositionUseCase.Create faz o seguinte: primeiro valida que a quantidade e o preço são positivos (regra de negócio, não validação de HTTP). Depois verifica se a criptomoeda existe no sistema (não faz sentido criar uma posição em uma crypto que não está cadastrada). Então calcula o InvestedAmount multiplicando quantidade pelo preço (campo derivado, lógica de negócio). Define o status como "active" (estado inicial). E finalmente persiste via interface, sem saber qual banco está por trás.

O Close é ainda mais interessante: busca a posição, verifica se pertence ao usuário que está tentando fechar (ownership), verifica se já não está fechada (regra de estado), e só então atualiza. São 4 regras de negócio em um método de 15 linhas. Tudo testável sem banco, sem HTTP, sem nada externo.

E o PortfolioUseCase mostra o poder da composição: ele cruza dados de posições (PostgreSQL), profit takes (PostgreSQL), preços atuais (InfluxDB) e cadastro de cryptos (PostgreSQL) para montar um resumo com lucro/prejuízo percentual de cada posição. São 4 fontes de dados, 2 bancos diferentes, e o use case não importa nenhum pacote de infraestrutura.

Testabilidade: O Benefício que Paga Todo o Esforço

Muita gente acha que Clean Architecture é sobre "organização" ou "ficar bonito". Não é. O benefício real, o que paga todo o esforço de separar camadas, é a testabilidade.

Como tudo depende de interfaces, testar um use case é trivial: você cria um mock que implementa a interface (um struct com um slice interno que simula o banco), instancia o use case com esse mock, e testa. Sem banco rodando. Sem Docker. Sem setup de ambiente. Sem variáveis de ambiente. Os testes rodam em 2 milissegundos.

No projeto, os testes verificam coisas como: "se eu criar uma posição com quantidade 0.5 e preço 50000, o invested_amount tem que ser 25000". Ou: "se o user 999 tentar fechar uma posição do user 1, tem que dar erro de unauthorized". São regras de negócio sendo testadas de forma isolada, rápida e confiável.

E o melhor: quando um teste quebra, você sabe exatamente onde está o problema. Não é "alguma coisa no fluxo HTTP que passa pelo middleware que chama o banco que retorna errado". É "a regra X do use case Y não está funcionando". Debugging cirúrgico.

Quando NÃO Usar Clean Architecture

Sendo honesto: se seu projeto é um microserviço com 3 endpoints que faz CRUD puro sem regra de negócio, Clean Architecture é overhead. Você vai criar 4 arquivos para fazer o que 1 faria. Vai ter uma interface que tem uma única implementação e nunca vai ser trocada. Vai ter um use case que só repassa a chamada para o repositório sem fazer nada.

Use Clean Architecture quando você tem regras de negócio reais (cálculos, validações de estado, verificação de ownership), quando usa múltiplas fontes de dados (2 ou mais bancos, APIs externas), quando precisa de testabilidade sem infraestrutura, ou quando o projeto vai crescer e ter mais desenvolvedores.

No Trivium faz sentido porque tem cálculo de P&L, verificação de ownership, composição de dados de PostgreSQL com InfluxDB com Binance WebSocket, e price alerts com lógica de disparo automático. Não é um CRUD simples.

Considerações Finais

No enfrentamento da complexidade de projetos que crescem, é crucial reconhecer que arquitetura não é sobre seguir um diagrama bonito do Uncle Bob. É sobre tomar decisões que facilitam a manutenção, os testes e a evolução do software ao longo do tempo. Clean Architecture é uma dessas ferramentas, mas como toda ferramenta, tem hora certa de usar.

O que eu quero que você leve deste artigo é o princípio por trás: separe o que é regra de negócio do que é detalhe de implementação. Se você fizer só isso, já está 80% do caminho andado. O resto (pastas, interfaces, DI) são consequências naturais desse princípio.

E lembre-se: pragmatismo acima de purismo. Se colocar uma tag db na entity resolve seu problema sem criar complexidade, faça. Se um use case com 3 linhas não justifica existir, não crie. A arquitetura serve ao projeto, não o contrário.


Links indicativos: