$ cat clean-architecture-con-go.md
Clean Architecture con Go
¿Qué es Clean Architecture?
El Diagrama de Capas
Las 4 Capas Principales
| Capa | Responsabilidad | En Go sería... |
|------|-----------------|----------------|
| Entidades | Reglas de negocio globales | Structs con métodos de validación |
| Casos de Uso | Reglas específicas de la aplicación | Servicios/Interactors |
| Repositorios (Interfaces) | Contratos para acceso a datos | Interfaces type Repository interface{} |
| Frameworks/Drivers | Detalles externos | Implementaciones concretas (GORM, SQL, HTTP) |
La Regla de Dependencia
// ✅ CORRECTO: El Core define la interfaz
// internal/core/repositories/user_repository.go
type UserRepository interface {
Save(user *User) error
FindByID(id string) (*User, error)
}
// ✅ CORRECTO: La capa externa IMPLEMENTA la interfaz
// internal/infrastructure/database/postgres/user_repository.go
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) Save(user *User) error {
// Implementación específica de PostgreSQL
}
// ❌ INCORRECTO: El Core conoce detalles externos
type UserService struct {
db *sql.DB // ¡Esto es un detalle de infraestructura!
}
Estructura de Proyecto Recomendada
mi-proyecto-go/
├── cmd/
│ └── api/
│ └── main.go # Punto de entrada (inyección de dependencias)
├── internal/
│ ├── core/ # CAPA CENTRAL (sin dependencias externas)
│ │ ├── entities/ # Entidades de negocio
│ │ │ └── user.go
│ │ ├── repositories/ # Interfaces de repositorios
│ │ │ └── user_repository.go
│ │ └── services/ # Casos de uso
│ │ └── user_service.go
│ ├── handlers/ # CAPA DE INTERFACES (HTTP/gRPC)
│ │ └── user_handler.go
│ └── infrastructure/ # CAPA EXTERNA (implementaciones concretas)
│ ├── database/
│ │ ├── postgres/
│ │ │ └── user_repository.go
│ │ └── redis/
│ │ └── cache_repository.go
│ └── web/
│ └── server.go
├── pkg/ # Código reutilizable (opcional)
│ └── errors/
│ └── custom_errors.go
├── go.mod
└── go.sum
Implementación Paso a Paso
1. Entidades (Core - Capa más interna)
// internal/core/entities/user.go
package entities
import (
"errors"
"regexp"
)
type User struct {
ID string
Email string
Password string // Hasheado, nunca en texto plano
IsActive bool
}
// Reglas de negocio: validación del email
func (u *User) ValidateEmail() error {
if u.Email == "" {
return errors.New("email es requerido")
}
emailRegex := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$`)
if !emailRegex.MatchString(u.Email) {
return errors.New("email inválido")
}
return nil
}
// Regla de negocio: activar usuario
func (u *User) Activate() error {
if u.IsActive {
return errors.New("usuario ya está activo")
}
u.IsActive = true
return nil
}
// Regla de negocio: desactivar usuario
func (u *User) Deactivate() error {
if !u.IsActive {
return errors.New("usuario ya está inactivo")
}
u.IsActive = false
return nil
}
2. Interfaces de Repositorios (Core)
// internal/core/repositories/user_repository.go
package repositories
import (
"context"
"mi-proyecto/internal/core/entities"
)
type UserRepository interface {
Save(ctx context.Context, user *entities.User) error
FindByID(ctx context.Context, id string) (*entities.User, error)
FindByEmail(ctx context.Context, email string) (*entities.User, error)
Delete(ctx context.Context, id string) error
Update(ctx context.Context, user *entities.User) error
}
3. Casos de Uso (Servicios)
// internal/core/services/user_service.go
package services
import (
"context"
"errors"
"mi-proyecto/internal/core/entities"
"mi-proyecto/internal/core/repositories"
"golang.org/x/crypto/bcrypt"
)
type UserService struct {
repo repositories.UserRepository
}
// Constructor con inyección de dependencias
func NewUserService(repo repositories.UserRepository) *UserService {
return &UserService{repo: repo}
}
// Caso de uso: Crear usuario
func (s *UserService) CreateUser(ctx context.Context, email, password string) (*entities.User, error) {
// 1. Validar email
user := &entities.User{Email: email}
if err := user.ValidateEmail(); err != nil {
return nil, err
}
// 2. Verificar si ya existe
existing, _ := s.repo.FindByEmail(ctx, email)
if existing != nil {
return nil, errors.New("el email ya está registrado")
}
// 3. Hashear password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user.Password = string(hashedPassword)
user.IsActive = true
// 4. Generar ID (simplificado)
user.ID = generateID() // Implementar con UUID
// 5. Guardar
if err := s.repo.Save(ctx, user); err != nil {
return nil, err
}
return user, nil
}
// Caso de uso: Obtener usuario por ID
func (s *UserService) GetUser(ctx context.Context, id string) (*entities.User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, errors.New("usuario no encontrado")
}
return user, nil
}
// Caso de uso: Desactivar usuario
func (s *UserService) DeactivateUser(ctx context.Context, id string) error {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return errors.New("usuario no encontrado")
}
if err := user.Deactivate(); err != nil {
return err
}
return s.repo.Update(ctx, user)
}
4. Implementación de Repositorio (Infraestructura)
// internal/infrastructure/database/postgres/user_repository.go
package postgres
import (
"context"
"database/sql"
"mi-proyecto/internal/core/entities"
"mi-proyecto/internal/core/repositories"
_ "github.com/lib/pq"
)
type PostgresUserRepository struct {
db *sql.DB
}
// Constructor
func NewPostgresUserRepository(connString string) (*PostgresUserRepository, error) {
db, err := sql.Open("postgres", connString)
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
return nil, err
}
return &PostgresUserRepository{db: db}, nil
}
// Aseguramos que implementa la interfaz
var _ repositories.UserRepository = (*PostgresUserRepository)(nil)
func (r *PostgresUserRepository) Save(ctx context.Context, user *entities.User) error {
query := `INSERT INTO users (id, email, password, is_active) VALUES ($1, $2, $3, $4)`
_, err := r.db.ExecContext(ctx, query, user.ID, user.Email, user.Password, user.IsActive)
return err
}
func (r *PostgresUserRepository) FindByID(ctx context.Context, id string) (*entities.User, error) {
query := `SELECT id, email, password, is_active FROM users WHERE id = $1`
row := r.db.QueryRowContext(ctx, query, id)
var user entities.User
err := row.Scan(&user.ID, &user.Email, &user.Password, &user.IsActive)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
}
// Implementar otros métodos (FindByEmail, Delete, Update)...
5. Handlers HTTP (Capa de Interfaces)
// internal/handlers/user_handler.go
package handlers
import (
"encoding/json"
"net/http"
"mi-proyecto/internal/core/services"
)
type UserHandler struct {
userService *services.UserService
}
func NewUserHandler(userService *services.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
type CreateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func (h *UserHandler) 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(r.Context(), req.Email, req.Password)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
6. Main (Inyección de Dependencias)
// cmd/api/main.go
package main
import (
"log"
"net/http"
"mi-proyecto/internal/core/services"
"mi-proyecto/internal/handlers"
"mi-proyecto/internal/infrastructure/database/postgres"
)
func main() {
// 1. Inicializar dependencias externas (infraestructura)
userRepo, err := postgres.NewPostgresUserRepository("postgres://user:pass@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.Fatal("Error connecting to database:", err)
}
// 2. Inyectar dependencias en los casos de uso
userService := services.NewUserService(userRepo)
// 3. Inyectar casos de uso en los handlers
userHandler := handlers.NewUserHandler(userService)
// 4. Configurar rutas
http.HandleFunc("/users", userHandler.CreateUser)
// 5. Iniciar servidor
log.Println("Server running on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
¿Cuándo USAR Clean Architecture?
✅ Casos Ideales
| Escenario | ¿Por qué funciona bien? | |-----------|------------------------| | Microservicios | Cada servicio tiene su propia lógica de negocio claramente aislada | | Aplicaciones con reglas de negocio complejas | El core se mantiene puro y testeable sin dependencias externas | | Equipos grandes | Las capas definen contratos claros que facilitan el trabajo paralelo | | Proyectos a largo plazo (> 1 año) | El mantenimiento y los cambios son predecibles | | Posible cambio de tecnologías | Cambiar de base de datos o framework no afecta la lógica de negocio | | Alta necesidad de pruebas | El core se prueba sin bases de datos ni APIs reales |
❌ Cuándo NO usarla (Over-engineering)
| Escenario | Alternativa recomendada | |-----------|------------------------| | Prototipos o MVPs | Usa una estructura más simple (package by feature) | | Scripts pequeños o CLIs | Un solo archivo o paquete plano | | APIs CRUD simples sin lógica compleja | Go estándar con repositorios simples | | Equipo pequeño con plazos ajustados | La complejidad inicial no vale la pena | | Proyectos de corta duración (< 3 meses) | Concéntrate en entregar valor rápido |
Beneficios Clave
1. Testeabilidad Excepcional
// tests/core/services/user_service_test.go
package services_test
import (
"context"
"testing"
"mi-proyecto/internal/core/entities"
"mi-proyecto/internal/core/services"
)
// Mock del repositorio (fácil de crear porque es una interfaz)
type MockUserRepository struct {
users map[string]*entities.User
}
func (m *MockUserRepository) Save(ctx context.Context, user *entities.User) error {
m.users[user.ID] = user
return nil
}
func TestCreateUser(t *testing.T) {
// Arrange
mockRepo := &MockUserRepository{users: make(map[string]*entities.User)}
userService := services.NewUserService(mockRepo)
// Act
user, err := userService.CreateUser(context.Background(), "test@example.com", "password123")
// Assert
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if user.Email != "test@example.com" {
t.Errorf("Expected email test@example.com, got %s", user.Email)
}
}
2. Cambiar Base de Datos es Fácil
// Solo necesitas crear una nueva implementación de UserRepository
// internal/infrastructure/database/mongodb/user_repository.go
type MongoUserRepository struct {
collection *mongo.Collection
}
// Implementar la misma interfaz...
// Luego en main.go, cambiar solo una línea:
// userRepo := postgres.NewPostgresUserRepository(...)
// 👇 por
// userRepo := mongodb.NewMongoUserRepository(...)
3. Frameworks Son Intercambiables
¿Cambiar de Gin a Echo o Fiber? Solo afecta a la capa de handlers. El core y los casos de uso siguen intactos.
Patrón Avanzado: Unit of Work
Para operaciones que involucran múltiples repositorios:
// internal/core/repositories/unit_of_work.go
type UnitOfWork interface {
Users() UserRepository
Orders() OrderRepository
Commit() error
Rollback() error
}
// Caso de uso complejo
func (s *OrderService) CreateOrderWithUser(ctx context.Context, userEmail string, orderData OrderData) error {
uow := s.uowFactory.New()
defer uow.Rollback() // Rollback automático si algo falla
user, err := uow.Users().FindByEmail(ctx, userEmail)
if err != nil {
return err
}
order := entities.NewOrder(user.ID, orderData)
if err := uow.Orders().Save(ctx, order); err != nil {
return err
}
return uow.Commit() // Solo commitea si todo está bien
}
Errores Comunes y Cómo Evitarlos
❌ Error 1: El Core importa paquetes externos
// ❌ MAL - El core conoce a GORM
import "gorm.io/gorm"
type UserRepository interface {
FindByID(db *gorm.DB, id string) // ¡No! La interfaz no debe recibir detalles de implementación
}
✅ Solución: Definir interfaces puras
// ✅ BIEN
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error) // Sin rastros de GORM
}
❌ Error 2: Poner lógica de negocio en handlers
// ❌ MAL
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
// Validación de email aquí... ¡No! Esto es lógica de negocio
if !strings.Contains(req.Email, "@") {
return error
}
}
❌ Error 3: Demasiadas capas para casos simples
Para un CRUD simple, no necesitas 4 capas. A veces un handler + repositorio directo es suficiente.
Conclusión
Clean Architecture en Go no es una bala de plata, pero es una herramienta poderosa cuando se usa en el contexto correcto. La clave está en:
- Mantener el core puro - Sin dependencias externas
- Depender de abstracciones, no de implementaciones - Las interfaces definen contratos
- Invertir las dependencias - La infraestructura depende del core, no al revés
- Usarla donde importe - No sobreingeniería para proyectos simples
Recuerda: La arquitectura debe facilitar el desarrollo, no entorpecerlo. Empieza simple y añade complejidad solo cuando la necesites.
Recursos Adicionales
¿Tienes experiencia con Clean Architecture en Go? ¡Comparte tus aprendizajes en los comentarios! 👇