Por que Engenheiros Sênior Estão Abandonando MVC em Go
E aí, pessoal!
Hoje vou te mostrar por que engenheiros sênior estão abandonando o padrão MVC em Go e migrando para arquiteturas mais modernas. Se você ainda está forçando Go em um padrão que não combina com a linguagem, este post é para você!
O Problema: MVC Não Foi Feito para Go
MVC (Model-View-Controller) nasceu em frameworks como Rails e Spring, quando o mundo era diferente:
- Monolitos eram a norma
- Server-side rendering dominava
- Controllers complexos controlavam tudo
- Models faziam tudo (dados + comportamento)
Mas Go é diferente. Go não é uma linguagem orientada a frameworks. Go é sobre simplicidade, composição e interfaces.
Por que MVC Falha em Go?
1. Go Não Tem “Controllers” Nativos
O problema fundamental: Go não foi projetado para o padrão MVC tradicional. Quando você tenta forçar MVC em Go, acaba criando controllers que fazem tudo:
- Validação de dados
- Lógica de negócio
- Persistência de dados
- Serialização de resposta
Por que isso é problemático:
- Controllers complexos: Violam o princípio da responsabilidade única
- Tight coupling: Acoplamento forte entre camadas
- Difícil de testar: Muitas dependências e responsabilidades
- Violação de responsabilidades: Controller faz trabalho que deveria ser de Service/Repository
Em produção, isso resulta em:
- Bugs difíceis de rastrear
- Código que ninguém quer mexer
- Testes que quebram por qualquer mudança
- Refatorações que viram pesadelos
2. Models em Go São Diferentes
O problema: Em frameworks como Rails/Spring, models fazem tudo - validação, persistência, lógica de negócio, até envio de emails. Em Go, isso vira um pesadelo.
Por que não funciona:
- Violação SRP: Model sobrecarregado com muitas responsabilidades
- Difícil de testar: Como testar um model que faz 5 coisas diferentes?
- Acoplamento: Model conhece banco, email, validação, etc.
- Reutilização: Como reutilizar lógica que está misturada no model?
Em sistemas reais, isso causa:
- Models com 500+ linhas de código
- Testes que precisam mockar 10 dependências
- Mudanças que quebram funcionalidades não relacionadas
- Código que ninguém entende completamente
3. Views Não Existem em APIs
O problema: MVC foi criado para aplicações web com server-side rendering. Em APIs REST/GraphQL, o conceito de “View” não faz sentido.
Por que é problemático:
- Desnecessário: APIs retornam JSON, não HTML
- Complexidade extra: Camada que não agrega valor
- Confusão: Mistura conceitos de web tradicional com API moderna
- Overhead: Código extra para algo que o framework já faz
Em APIs modernas:
- Serialização é feita automaticamente pelo framework
- Lógica de apresentação é responsabilidade do cliente
- “Views” viram apenas wrappers desnecessários
Cenários Reais: Quando MVC Quebra
Sistema de E-commerce
Problema: Controller de pedidos com 800 linhas
- Validação de produtos
- Cálculo de impostos
- Integração com gateway de pagamento
- Envio de emails
- Atualização de estoque
Resultado: 6 meses para adicionar uma nova forma de pagamento
API de Microserviços
Problema: 15 microserviços, todos com MVC
- Cada controller faz 5-10 coisas diferentes
- Testes que demoram 2 horas para rodar
- Bugs que aparecem em serviços não relacionados
Resultado: Time de 8 pessoas, 80% do tempo em manutenção
Sistema Financeiro
Problema: Model de transação com 20 responsabilidades
- Validação de regras de negócio
- Cálculos complexos
- Persistência em múltiplos bancos
- Auditoria e logs
Resultado: 3 meses para implementar uma nova regra de compliance
O Que Engenheiros Sênior Estão Fazendo
1. Hexagonal Architecture (Ports & Adapters)
Se você quer aprender Hexagonal Architecture em profundidade, gravei um vídeo no YouTube sobre o tema:
A ideia central: Coloque sua lógica de negócio no centro e conecte tudo através de interfaces (ports). As implementações (adapters) ficam na periferia.
Por que funciona em Go:
- Interfaces implícitas: Go não precisa declarar que implementa uma interface
- Composição: Fácil de compor comportamentos
- Testabilidade: Interfaces são fáceis de mockar
- Flexibilidade: Troca implementações sem quebrar código
Estrutura típica:
- Domain: Entidades e regras de negócio
- Ports: Interfaces que definem contratos
- Adapters: Implementações específicas (banco, HTTP, etc.)
- Use Cases: Orquestração da lógica de negócio
Resultados em produção:
- 90% menos bugs em mudanças de infraestrutura
- 3x mais rápido para adicionar novas features
- Testes que rodam em segundos ao invés de minutos
- Código que qualquer dev entende em 5 minutos
2. Clean Architecture
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ✅ Entities (regras de negócio)
type User struct {
ID string
Name string
Email string
}
func (u *User) Validate() error {
if u.Name == "" {
return errors.New("name is required")
}
if !isValidEmail(u.Email) {
return errors.New("invalid email")
}
return nil
}
// ✅ Use Cases (aplicação)
type UserService struct {
repo UserRepository
}
func (s *UserService) CreateUser(name, email string) (*User, error) {
user := &User{
ID: generateID(),
Name: name,
Email: email,
}
if err := user.Validate(); err != nil {
return nil, err
}
return user, s.repo.Save(user)
}
// ✅ Interface Adapters (infraestrutura)
type HTTPHandler struct {
userService *UserService
}
func (h *HTTPHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
user, err := h.userService.CreateUser(req.Name, req.Email)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
Vantagens:
- Independência: Lógica de negócio não depende de frameworks
- Testabilidade: Cada camada é testável independentemente
- Flexibilidade: Fácil de trocar implementações
- Manutenibilidade: Mudanças isoladas por camada
3. Domain-Driven Design (DDD)
A ideia central: Modele seu código baseado no domínio de negócio real. Use tipos seguros e coloque a lógica de negócio onde ela pertence.
Por que funciona em Go:
- Type safety: Go tem tipos fortes que previnem erros
- Interfaces: Fácil de definir contratos de domínio
- Composição: Fácil de compor comportamentos complexos
- Simplicidade: Go força você a ser simples e claro
Conceitos principais:
- Entities: Objetos com identidade única
- Value Objects: Objetos imutáveis sem identidade
- Aggregates: Grupos de entidades relacionadas
- Domain Services: Lógica que não pertence a uma entidade
Resultados em produção:
- Menos bugs: Type safety previne erros comuns
- Código autodocumentado: Nomes refletem o domínio real
- Facilita comunicação: Devs e negócio falam a mesma linguagem
- Evolução natural: Código evolui com o domínio
Comparação: MVC vs Arquiteturas Modernas
MVC em Go (❌)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Controller faz tudo
type UserController struct {
db *sql.DB
}
func (c *UserController) CreateUser(w http.ResponseWriter, r *http.Request) {
// 1. Parse request
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req)
// 2. Validação
if req.Name == "" {
http.Error(w, "name required", 400)
return
}
// 3. Lógica de negócio
user := &User{
ID: generateID(),
Name: req.Name,
Email: req.Email,
}
// 4. Persistência
_, err := c.db.Exec("INSERT INTO users...", user.ID, user.Name, user.Email)
if err != nil {
http.Error(w, "database error", 500)
return
}
// 5. Resposta
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
Problemas:
- Tudo no controller: Violação de responsabilidades
- Difícil de testar: Muitas dependências
- Acoplamento: Controller conhece banco, HTTP, etc.
- Duplicação: Lógica espalhada em vários controllers
Arquitetura Moderna (✅)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Separação clara de responsabilidades
type CreateUserHandler struct {
useCase CreateUserUseCase
}
func (h *CreateUserHandler) Handle(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
user, err := h.useCase.Execute(req.Name, req.Email)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// Use case isolado e testável
type CreateUserUseCase struct {
repo UserRepository
}
func (uc *CreateUserUseCase) Execute(name, email string) (*User, error) {
user, err := NewUser(name, email)
if err != nil {
return nil, err
}
return user, uc.repo.Save(user)
}
Vantagens:
- Responsabilidades claras: Cada classe tem uma responsabilidade
- Fácil de testar: Use case pode ser testado isoladamente
- Baixo acoplamento: Handler não conhece implementação
- Reutilização: Use case pode ser usado em outros contextos
Trade-offs: Custo vs Benefício
Custo da Migração
- Tempo: 2-4 semanas para migrar um módulo médio
- Risco: Possibilidade de introduzir bugs
- Curva de aprendizado: Time precisa aprender novos padrões
- Complexidade inicial: Mais código para escrever no início
Benefícios a Longo Prazo
- Manutenibilidade: 60% menos tempo para fazer mudanças
- Testabilidade: 80% mais cobertura de testes
- Escalabilidade: Fácil adicionar novos desenvolvedores
- Qualidade: 70% menos bugs em produção
ROI da Migração
- Break-even: 3-6 meses após migração
- Payback: 2-3x mais produtividade após 1 ano
- Redução de custos: 40% menos tempo em manutenção
Quando Usar Cada Abordagem
Use MVC Quando:
- Protótipos rápidos: Desenvolvimento rápido (1-2 semanas)
- APIs simples: CRUD básico sem lógica complexa
- Equipe pequena: 1-2 desenvolvedores experientes
- Deadline apertado: Prazo muito curto (< 1 mês)
- Sistema legado: Migração seria muito cara
Use Arquiteturas Modernas Quando:
- Sistemas complexos: Lógica de negócio complexa
- Equipe grande: 3+ desenvolvedores
- Longo prazo: Projeto de 6+ meses
- Alta qualidade: Necessidade de alta confiabilidade
- Múltiplas integrações: Muitas dependências externas
Sinais de que Precisa Migrar
Métricas de Código
- Controllers com 200+ linhas: Indica muitas responsabilidades
- Models com 300+ linhas: Lógica de negócio misturada
- Cobertura de testes < 60%: Difícil de testar
- Cyclomatic complexity > 10: Código muito complexo
Métricas de Processo
- Tempo de feature > 2 semanas: Para funcionalidades simples
- Bugs em produção > 5 por mês: Por módulo
- Tempo de onboarding > 2 semanas: Para novos desenvolvedores
- Refatorações que quebram: Funcionalidades não relacionadas
Sinais de Equipe
- “Não mexe nisso”: Desenvolvedores evitam certos módulos
- “Funciona, mas não sei como”: Código que ninguém entende
- Bugs recorrentes: Mesmos problemas aparecendo
- Deploy com medo: Medo de fazer mudanças
Migração Gradual
Fase 1: Extrair Use Cases
1
2
3
4
5
6
7
8
9
10
// Antes: Controller complexo
func (c *UserController) CreateUser(w http.ResponseWriter, r *http.Request) {
// Toda lógica aqui
}
// Depois: Controller + Use Case
func (c *UserController) CreateUser(w http.ResponseWriter, r *http.Request) {
user, err := c.createUserUseCase.Execute(name, email)
// Apenas HTTP handling
}
Fase 2: Introduzir Interfaces
1
2
3
4
5
6
7
8
9
// Antes: Dependência direta
type UserService struct {
db *sql.DB
}
// Depois: Interface
type UserService struct {
repo UserRepository
}
Fase 3: Aplicar DDD
1
2
3
4
5
6
7
8
9
10
11
12
13
// Antes: Struct simples
type User struct {
ID int
Name string
Email string
}
// Depois: Rich Domain
type User struct {
id UserID
name UserName
email Email
}
Ferramentas e Bibliotecas
Para Hexagonal Architecture:
- Wire: Dependency injection
- Testify: Mocking
- Gomock: Interface mocking
Para Clean Architecture:
- Clean Architecture: Estrutura de pastas
- Domain Events: Eventos de domínio
- CQRS: Command Query Responsibility Segregation
Para DDD:
- Value Objects: Tipos seguros
- Aggregates: Agregados de domínio
- Domain Events: Eventos de domínio
Conclusão
MVC não é o problema - o problema é forçar Go em um padrão que não combina com a linguagem.
Engenheiros sênior estão migrando para:
- Hexagonal Architecture: Para testabilidade e flexibilidade
- Clean Architecture: Para independência de frameworks
- DDD: Para domínios ricos e type safety
Principais benefícios:
- Testabilidade: Fácil de testar cada camada
- Manutenibilidade: Mudanças isoladas
- Flexibilidade: Troca implementações facilmente
- Escalabilidade: Arquitetura que escala com o time
Dica final: Não mude tudo de uma vez. Comece extraindo use cases, depois introduza interfaces, e gradualmente aplique DDD. A migração deve ser incremental e baseada nas necessidades reais do projeto.
A chave é escolher a arquitetura certa para o problema certo. MVC pode funcionar para protótipos, mas para sistemas complexos e de longo prazo, arquiteturas modernas são a escolha dos engenheiros sênior.
Referências
- Hexagonal Architecture em Go - Playlist Completa - Playlist completa sobre Hexagonal Architecture em Go
- Clean Architecture - Arquitetura limpa por Robert C. Martin
- Hexagonal Architecture - Arquitetura hexagonal por Alistair Cockburn
- Domain-Driven Design - DDD por Eric Evans
- Go Project Layout - Layout padrão para projetos Go