M02·02Arquitetura de software

CAPÍTULO 02

Arquitetura de software

Controller, Service, Repository, Clean Architecture e Hexagonal — os modelos que fazem código sobreviver a mudanças.

Por Thiago Souza12 min de leituraAtualizado em 2026-05

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:

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).

go
// 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
}
go
// 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.