cd ../blog
clean-architecture-con-go.md

$ cat clean-architecture-con-go.md

Clean Architecture con Go

5 de abril de 2026
goclean architecture

¿Qué es Clean Architecture?

El Diagrama de Capas

flowchart TD subgraph External["🔌 Capas Externas (Detalles)"] DB[(Base de Datos)] API["API Externa"] Web["Framework Web"] CLI["Interfaz CLI"] end subgraph Interface["🖥️ Capa de Interfaces"] direction LR Handlers["Handlers/Controllers"] Presenters["Presenters"] end subgraph UseCase["⚙️ Capa de Casos de Uso"] Logic["Lógica de Negocio"] Entities["Entidades"] end subgraph Core["💎 Capa Central (Políticas)"] direction LR Models["Modelos de Negocio"] Interfaces["Repositorios (Interfaces)"] end External --> Interface Interface --> UseCase UseCase --> Core note1["✅ Las dependencias apuntan hacia adentro\n❌ El Core NO conoce a las capas externas"]

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:

  1. Mantener el core puro - Sin dependencias externas
  2. Depender de abstracciones, no de implementaciones - Las interfaces definen contratos
  3. Invertir las dependencias - La infraestructura depende del core, no al revés
  4. 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! 👇