Go Admin

Clean Architecture em Go: Organizando Projetos para Escalar: Do Básico ao Avançado Já leu

O que é Clean Architecture Clean Architecture é um conjunto de princípios de design que visa criar sistemas de software independentes de frameworks, testáveis, independentes de interface de usuário, independentes de banco de dados e independentes de qualquer agente externo. Proposto por Robert C. Martin (Uncle Bob), o padrão organiza o código em camadas concêntricas, onde as dependências sempre apontam para dentro. Cada camada tem responsabilidades bem definidas, e a camada mais interna (domínio) nunca conhece a camada mais externa (frameworks e drivers). Em Go, a implementação de Clean Architecture ganha características próprias da linguagem, como a preferência por interfaces implícitas e composição sobre herança. O objetivo prático é criar projetos que crescem sem dificuldade, onde modificar uma dependência externa (como trocar de banco de dados) não quebra toda a lógica de negócio. Isso economiza tempo de manutenção e facilita testes automatizados significativamente. Por que Clean Architecture importa em Go Go é uma linguagem excelente para backend, mas projetos sem

O que é Clean Architecture

Clean Architecture é um conjunto de princípios de design que visa criar sistemas de software independentes de frameworks, testáveis, independentes de interface de usuário, independentes de banco de dados e independentes de qualquer agente externo. Proposto por Robert C. Martin (Uncle Bob), o padrão organiza o código em camadas concêntricas, onde as dependências sempre apontam para dentro. Cada camada tem responsabilidades bem definidas, e a camada mais interna (domínio) nunca conhece a camada mais externa (frameworks e drivers).

Em Go, a implementação de Clean Architecture ganha características próprias da linguagem, como a preferência por interfaces implícitas e composição sobre herança. O objetivo prático é criar projetos que crescem sem dificuldade, onde modificar uma dependência externa (como trocar de banco de dados) não quebra toda a lógica de negócio. Isso economiza tempo de manutenção e facilita testes automatizados significativamente.

Por que Clean Architecture importa em Go

Go é uma linguagem excelente para backend, mas projetos sem estrutura clara viram um "spaghetti code" rapidamente. Clean Architecture força decisões de design que Go naturalmente incentiva: simplicidade, clareza e separação de responsabilidades. Um projeto bem estruturado em Go é altamente manutenível porque a linguagem tem pouca "magia" — tudo é explícito.

As Camadas da Clean Architecture

A Clean Architecture divide-se em 4 camadas principais, cada uma com responsabilidades específicas. O fluxo de dependências sempre aponta para dentro, nunca sai da esfera central.

Camada de Domínio (Entities)

Esta é a camada mais interna e contém as entidades do negócio — a lógica que não mudaria nem se você trocar de web framework ou banco de dados. São estruturas simples de dados e funções que implementam regras de negócio fundamentais. Essa camada não deve importar nada de fora dela.

// domain/user.go
package domain

import "errors"

// User representa a entidade de usuário
type User struct {
    ID    string
    Name  string
    Email string
    Age   int
}

// NewUser cria um novo usuário com validações de negócio
func NewUser(id, name, email string, age int) (*User, error) {
    if name == "" {
        return nil, errors.New("nome é obrigatório")
    }
    if age < 18 {
        return nil, errors.New("usuário deve ser maior de 18 anos")
    }
    if !isValidEmail(email) {
        return nil, errors.New("email inválido")
    }

    return &User{
        ID:    id,
        Name:  name,
        Email: email,
        Age:   age,
    }, nil
}

func isValidEmail(email string) bool {
    // Validação simples
    return len(email) > 5 && len(email) < 254
}

Camada de Casos de Uso (Use Cases)

Aqui vivem os interatores (use cases) que orquestram a lógica de negócio. Um caso de uso representa uma ação específica do sistema — por exemplo, "registrar novo usuário" ou "atualizar perfil". Essa camada conhece a camada de domínio, mas não conhece detalhes de implementação como HTTP ou banco de dados.

// usecase/register_user.go
package usecase

import (
    "context"
    "github.com/seu-usuario/seu-projeto/domain"
)

// UserRepository define o contrato para persistência
type UserRepository interface {
    Save(ctx context.Context, user *domain.User) error
    FindByEmail(ctx context.Context, email string) (*domain.User, error)
}

// RegisterUserUseCase implementa o caso de uso de registro
type RegisterUserUseCase struct {
    userRepo UserRepository
}

func NewRegisterUserUseCase(repo UserRepository) *RegisterUserUseCase {
    return &RegisterUserUseCase{userRepo: repo}
}

// Execute executa o caso de uso
func (u *RegisterUserUseCase) Execute(ctx context.Context, 
    id, name, email string, age int) (*domain.User, error) {

    // Verifica se usuário já existe
    existing, _ := u.userRepo.FindByEmail(ctx, email)
    if existing != nil {
        return nil, ErrUserAlreadyExists
    }

    // Cria a entidade (validações de domínio acontecem aqui)
    user, err := domain.NewUser(id, name, email, age)
    if err != nil {
        return nil, err
    }

    // Persiste o usuário
    if err := u.userRepo.Save(ctx, user); err != nil {
        return nil, err
    }

    return user, nil
}

var ErrUserAlreadyExists = domain.NewDomainError("usuário com este email já existe")

Camada de Interface de Adaptadores (Adapters)

Essa camada contém os adaptadores que fazem a conversão entre o mundo externo (HTTP, gRPC, filas) e os casos de uso. Aqui vivem os controllers, presenters, gateways e implementações de repositórios. A camada conhece casos de uso mas não é conhecida por eles (inversão de controle).

// adapter/http/handler.go
package http

import (
    "encoding/json"
    "net/http"
    "github.com/seu-usuario/seu-projeto/usecase"
)

type RegisterUserHandler struct {
    registerUseCase *usecase.RegisterUserUseCase
}

func NewRegisterUserHandler(useCase *usecase.RegisterUserUseCase) *RegisterUserHandler {
    return &RegisterUserHandler{registerUseCase: useCase}
}

type RegisterUserRequest struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

func (h *RegisterUserHandler) Handle(w http.ResponseWriter, r *http.Request) {
    var req RegisterUserRequest

    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    user, err := h.registerUseCase.Execute(r.Context(), 
        req.ID, req.Name, req.Email, req.Age)

    if err != nil {
        w.WriteHeader(http.StatusUnprocessableEntity)
        json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
        return
    }

    response := UserResponse{
        ID:    user.ID,
        Name:  user.Name,
        Email: user.Email,
        Age:   user.Age,
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(response)
}

Camada de Frameworks e Drivers (Frameworks & DB)

A camada mais externa contém implementações específicas de banco de dados, frameworks web, logging, e outras dependências externas. Essa é a camada que muda mais frequentemente e deve ser a mais isolada possível do resto do código.

// adapter/storage/postgres_user_repository.go
package storage

import (
    "context"
    "database/sql"
    "github.com/seu-usuario/seu-projeto/domain"
    _ "github.com/lib/pq"
)

type PostgresUserRepository struct {
    db *sql.DB
}

func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
    return &PostgresUserRepository{db: db}
}

func (r *PostgresUserRepository) Save(ctx context.Context, user *domain.User) error {
    query := `INSERT INTO users (id, name, email, age) VALUES ($1, $2, $3, $4)`

    _, err := r.db.ExecContext(ctx, query, 
        user.ID, user.Name, user.Email, user.Age)

    return err
}

func (r *PostgresUserRepository) FindByEmail(ctx context.Context, 
    email string) (*domain.User, error) {

    query := `SELECT id, name, email, age FROM users WHERE email = $1`

    var user domain.User
    err := r.db.QueryRowContext(ctx, query, email).Scan(
        &user.ID, &user.Name, &user.Email, &user.Age)

    if err == sql.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }

    return &user, nil
}

Estrutura de Diretórios e Wiring

A organização física do projeto reflete as camadas lógicas. Um projeto bem estruturado em Go segue um padrão claro de diretórios, facilitando navegação e manutenção.

Estrutura de Pastas Recomendada

seu-projeto/
├── cmd/
│   └── main.go              # Entry point da aplicação
├── domain/                  # Entidades e lógica pura
│   ├── user.go
│   └── error.go
├── usecase/                 # Casos de uso / Interatores
│   ├── register_user.go
│   └── get_user.go
├── adapter/
│   ├── http/                # Controllers HTTP
│   │   ├── handler.go
│   │   └── middleware.go
│   └── storage/             # Implementações de repositórios
│       ├── postgres_user_repository.go
│       └── memory_user_repository.go
├── infra/                   # Configuração de DI e setup
│   └── wire.go
├── go.mod
└── go.sum

Inversão de Controle com Wire

Go não possui containers de DI nativos. A biblioteca wire do Google resolve isso gerando código de injeção de dependência em tempo de compilação.

// infra/wire.go
// +build wireinject

package infra

import (
    "database/sql"
    "github.com/google/wire"
    "github.com/seu-usuario/seu-projeto/adapter/http"
    "github.com/seu-usuario/seu-projeto/adapter/storage"
    "github.com/seu-usuario/seu-projeto/usecase"
)

func InitializeHandler(db *sql.DB) *http.RegisterUserHandler {
    wire.Build(
        storage.NewPostgresUserRepository,
        usecase.NewRegisterUserUseCase,
        http.NewRegisterUserHandler,
    )
    return nil // Wire gera a implementação real
}

Execute wire no diretório para gerar o código: wire ./infra/...

Testabilidade e Benefícios Práticos

Uma das maiores vantagens da Clean Architecture é a facilidade de testes. Como cada camada tem responsabilidades claras e usa interfaces, criar mocks é trivial.

Testando Casos de Uso

// usecase/register_user_test.go
package usecase

import (
    "context"
    "testing"
    "github.com/seu-usuario/seu-projeto/domain"
)

// MockUserRepository implementa UserRepository para testes
type MockUserRepository struct {
    SaveCalled bool
    Users      map[string]*domain.User
}

func (m *MockUserRepository) Save(ctx context.Context, user *domain.User) error {
    m.SaveCalled = true
    m.Users[user.Email] = user
    return nil
}

func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
    return m.Users[email], nil
}

func TestRegisterUserUseCase_Success(t *testing.T) {
    mockRepo := &MockUserRepository{Users: make(map[string]*domain.User)}
    useCase := NewRegisterUserUseCase(mockRepo)

    user, err := useCase.Execute(context.Background(), 
        "123", "João", "joao@example.com", 25)

    if err != nil {
        t.Fatalf("esperava sucesso, got %v", err)
    }

    if user.Name != "João" {
        t.Errorf("esperava João, got %s", user.Name)
    }

    if !mockRepo.SaveCalled {
        t.Error("esperava que Save fosse chamado")
    }
}

func TestRegisterUserUseCase_DuplicateEmail(t *testing.T) {
    mockRepo := &MockUserRepository{
        Users: map[string]*domain.User{
            "joao@example.com": {ID: "456", Name: "João Antigo"},
        },
    }
    useCase := NewRegisterUserUseCase(mockRepo)

    _, err := useCase.Execute(context.Background(), 
        "123", "João Novo", "joao@example.com", 25)

    if err != ErrUserAlreadyExists {
        t.Errorf("esperava ErrUserAlreadyExists, got %v", err)
    }
}

Trocar de Banco de Dados é Trivial

Se precisar trocar PostgreSQL por MongoDB, você só muda a implementação do repositório. Os casos de uso não sabem disso e nem o código HTTP.

// adapter/storage/mongo_user_repository.go
package storage

import (
    "context"
    "go.mongodb.org/mongo-driver/mongo"
    "github.com/seu-usuario/seu-projeto/domain"
)

type MongoUserRepository struct {
    collection *mongo.Collection
}

func NewMongoUserRepository(collection *mongo.Collection) *MongoUserRepository {
    return &MongoUserRepository{collection: collection}
}

func (r *MongoUserRepository) Save(ctx context.Context, user *domain.User) error {
    _, err := r.collection.InsertOne(ctx, user)
    return err
}

func (r *MongoUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
    var user domain.User
    err := r.collection.FindOne(ctx, map[string]string{"email": email}).Decode(&user)

    if err == mongo.ErrNoDocuments {
        return nil, nil
    }
    return &user, err
}

Agora na injeção de dependência, você só troca qual implementação usar. O resto do código não muda.

Conclusão

Clean Architecture em Go fornece uma estrutura sólida para projetos que precisam crescer. Os três pontos principais aprendidos são: (1) A separação em camadas concêntricas com dependências apontando para dentro garante que a lógica de negócio é independente de detalhes técnicos; (2) O uso de interfaces e inversão de controle torna testes automatizados simples e rápidos, porque substituir implementações reais por mocks é natural; (3) A estrutura de diretórios clara reflete as responsabilidades, facilitando onboarding de novos desenvolvedores e reduzindo tempo de manutenção quando mudanças externas (como trocar banco de dados) são necessárias.

Referências


Artigos relacionados