A grande analogia: o restaurante
Imagine um restaurante bem organizado:
- Garçom recebe o pedido do cliente. Não cozinha. Só anota e leva para a cozinha.
- Chef de cozinha sabe a receita. Pega ingredientes do estoque, monta o prato.
- Estoque guarda os ingredientes. Não sabe o que é receita.
Cada um faz uma coisa. Se um dia o restaurante decidir trocar o estoque (de geladeira para câmara fria), o chef não precisa aprender nada novo — ele continua pedindo "tomate". Se trocar o garçom por um app de pedido, o chef nem percebe.
Isso é separação de responsabilidades. E é a base de toda boa arquitetura.
Camadas (Controller, Service, Repository)
A arquitetura em camadas é o restaurante traduzido para código:
flowchart TD A["Controller / Handler\nGarçom — recebe requisições HTTP"] -->|"delega lógica"| B["Service\nChef — regras de negócio"] B -->|"consulta / persiste"| C["Repository\nEstoque — acesso ao banco"]
Regra de ouro: as setas só descem. Controller chama Service. Service chama Repository. Repository nunca chama Service. Service nunca chama Controller.
Exemplo curtinho em Go:
// controller (garçom): só lida com HTTP
func (c *UserController) Create(w http.ResponseWriter, r *http.Request) {
var input CreateUserInput
// 1. Decodifica a requisição (transforma JSON em struct)
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "json inválido", http.StatusBadRequest)
return
}
// 2. Delega o trabalho de verdade para o service
user, err := c.service.CreateUser(r.Context(), input)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 3. Devolve a resposta. Pronto, garçom serviu.
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
// service (chef): regras de negócio
func (s *UserService) CreateUser(ctx context.Context, in CreateUserInput) (User, error) {
// valida regras (e-mail único, senha forte, etc.)
if err := in.Validate(); err != nil {
return User{}, err
}
// gera hash da senha (regra de negócio, não de banco)
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if err != nil {
return User{}, err
}
user := User{Email: in.Email, PasswordHash: string(hash)}
// delega persistência para o repositório
return s.repo.Save(ctx, user)
}
// repository (estoque): só fala com banco
func (r *UserRepository) Save(ctx context.Context, u User) (User, error) {
query := `INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id`
err := r.db.QueryRowContext(ctx, query, u.Email, u.PasswordHash).Scan(&u.ID)
return u, err
}Note como cada camada faz uma coisa só. Isso é o que faz o código sobreviver a mudanças.
Clean Architecture
Analogia da cebola: Clean Architecture é como uma cebola. No miolo está a regra de negócio (o coração do sistema). Quanto mais para fora, mais "detalhes técnicos" — banco, HTTP, frameworks. A regra de ouro: dependências apontam sempre para dentro.
Por que isso importa? Porque o que mais muda em sistemas é o "lado de fora": hoje é Postgres, amanhã é DynamoDB. Hoje é REST, amanhã é gRPC. Se o miolo do seu sistema (a regra de negócio de verdade) não depende dessas coisas, você troca o "lado de fora" sem mexer no coração.
Na prática, isso vira interfaces. O caso de uso não conhece Postgres — ele conhece uma interface UserRepository. Quem implementa essa interface usando Postgres é um detalhe que mora "fora".
Hexagonal Architecture (Ports & Adapters)
Analogia das tomadas: seu computador tem uma entrada USB-C. Você pode plugar nela um carregador, um pendrive, um monitor, um cabo de rede. O computador não sabe (e não precisa saber) o que está plugado. Ele só conhece o formato da porta.
Hexagonal pensa o sistema do mesmo jeito:
- Ports (portas): interfaces que o seu núcleo expõe ou consome.
- Adapters (adaptadores): implementações concretas dessas portas.
Na prática, Clean Architecture e Hexagonal são muito parecidas. A diferença é mais de ênfase: Hexagonal foca em "qualquer coisa que entra ou sai do sistema é uma porta com adaptador". Clean foca em "camadas concêntricas com regras puras no centro".
Para um backend em Go normal, você não precisa escolher entre uma e outra. O que importa é internalizar o princípio:
Regra de negócio nunca depende de detalhe de infraestrutura.
Em Go, isso vira: defina interfaces no pacote do service, e implemente essas interfaces nos pacotes de infra (postgres, redis, http).
// pacote service (núcleo, puro)
package user
type Repository interface {
Save(ctx context.Context, u User) (User, error)
FindByEmail(ctx context.Context, email string) (User, error)
}
type Service struct {
repo Repository // depende da INTERFACE, não de Postgres
}// pacote postgres (infra, "lado de fora")
package postgres
// implementa user.Repository usando Postgres
type UserRepo struct { db *sql.DB }
func (r *UserRepo) Save(ctx context.Context, u user.User) (user.User, error) {
// ... SQL aqui
}Amanhã, se o time decidir trocar Postgres por MongoDB, você cria um mongo.UserRepo que também implementa user.Repository. O service nem fica sabendo.