Arquitectura Modular y Escalable en Go: Un Enfoque Práctico

GusGus
4 min read

Resumen

Este documento presenta una arquitectura modular y escalable para aplicaciones en Go, basada en una separación clara entre la lógica de negocio (domain/), la capa de aplicación (application/) y la infraestructura (infra/). Se explica cómo manejar entidades, repositorios y casos de uso de forma desacoplada, junto con una metodología clara para organizar usecases/ y services/ dentro de application/. Se proporcionan ejemplos prácticos y justificaciones sobre cada decisión arquitectónica.


1. Introducción

En proyectos grandes, el código tiende a volverse difícil de mantener debido al acoplamiento entre capas de negocio, aplicación e infraestructura. Este paper propone una estructura basada en modularidad y desacoplamiento, asegurando:

  • Separación clara entre capas (lógica de negocio, aplicación e infraestructura).

  • Código mantenible y extensible sin afectar otras partes del sistema.

  • Escalabilidad a medida que la aplicación crece.

Esta arquitectura ha sido probada en proyectos SaaS y garantiza un flujo limpio de datos y responsabilidades.


2. Estructura del Proyecto

La arquitectura se organiza en tres capas principales:

/project-root
│── /domain             # Lógica de negocio pura
│   │── user.go
│   │── user_repository.go
│
│── /application        # Casos de uso y coordinación de lógica
│   │── /usecases
│   │   │── user_usecase.go
│   │── /services
│   │   │── user_service.go
│
│── /infra             # Implementaciones concretas (persistencia, APIs, etc.)
│   │── user_repository.go
│
│── main.go            # Inicialización de dependencias

📌 Diferencias clave:

  • domain/ → Define reglas de negocio sin dependencias externas.

  • application/usecases/ → Define la lógica de aplicación sin tocar infraestructura.

  • application/services/ → Coordina múltiples usecases/ si es necesario.

  • infra/ → Implementa repositorios y adaptadores de infraestructura.

Este enfoque evita acoplamiento innecesario y permite crecer sin romper el código.


3. Implementación

A continuación, mostramos la implementación de cada capa con el caso de estudio de gestión de usuarios.


3.1 Definición de la Entidad (domain/user.go)

📌 User encapsula lógica de negocio. No tiene dependencias con repositorios ni infraestructura.

package domain

import "errors"

type User struct {
    ID     int64
    Name   string
    Email  string
    Active bool
}

// NewUser se usa cuando necesitas CREAR un usuario manualmente, por ejemplo:
// - Cuando un usuario se registra por primera vez en el sistema.
// - Cuando generas un usuario en código, como en pruebas unitarias.
// 🚨 NO uses esta función si el usuario viene de la BD, porque ya viene con datos.
func NewUser(id int64, name, email string) (*User, error) {
    if id <= 0 {
        return nil, errors.New("invalid user ID")
    }
    return &User{
        ID:     id,
        Name:   name,
        Email:  email,
        Active: true,
    }, nil
}

// Métodos de la entidad para modificar su estado.
func (u *User) Deactivate() { u.Active = false }
func (u *User) ResetEmail() { u.Email = "deactivated@example.com" }

📌 Cuándo usar NewUser y cuándo no:
NewUser() → Cuando se crea un usuario nuevo.
Carga desde el repositorio (GetByID) → Cuando el usuario ya existe.


3.2 Interfaz del Repositorio (domain/user_repository.go)

📌 Define cómo acceder a los datos, sin implementaciones concretas.

package domain

type UserRepository interface {
    GetByID(id int64) (*User, error)
    Save(user *User) error
    FindWithPagination(offset, limit int) ([]User, error)
    SearchByFilter(filters map[string]interface{}) ([]User, error)
}

📌 El UseCase solo depende de esta interfaz, evitando acoplarse a una base de datos específica.


3.3 Casos de Uso (application/usecases/user_usecase.go)

📌 UseCase contiene lógica de aplicación, pero no toca infraestructura.

package usecases

import "myapp/domain"

type UserUsecase struct {
    userRepo domain.UserRepository
}

func NewUserUsecase(userRepo domain.UserRepository) *UserUsecase {
    return &UserUsecase{userRepo: userRepo}
}

// 🔹 Obtener usuarios paginados
func (u *UserUsecase) FindWithPagination(offset, limit int) ([]domain.User, error) {
    return u.userRepo.FindWithPagination(offset, limit)
}

// 🔹 Desactivar usuario
func (u *UserUsecase) DeactivateUser(id int64) error {
    user, err := u.userRepo.GetByID(id)
    if err != nil {
        return err
    }
    user.Deactivate()
    user.ResetEmail()
    return u.userRepo.Save(user)
}

📌 Separa reglas de negocio de la infraestructura.
Nunca toca base de datos directamente.


3.4 Implementación del Repositorio (infra/user_repository.go)

📌 Aquí está la implementación real de la persistencia.

package infra

import "myapp/domain"

type UserRepositoryImpl struct {
    users map[int64]*domain.User
}

func NewUserRepository() *UserRepositoryImpl {
    return &UserRepositoryImpl{users: make(map[int64]*domain.User)}
}

func (r *UserRepositoryImpl) GetByID(id int64) (*domain.User, error) {
    user, exists := r.users[id]
    if !exists {
        return nil, errors.New("user not found")
    }
    return user, nil
}

func (r *UserRepositoryImpl) Save(user *domain.User) error {
    r.users[user.ID] = user
    return nil
}

📌 Infraestructura desacoplada de la lógica de negocio.


3.5 Servicio (application/services/user_service.go)

📌 Coordina múltiples UseCases, sin tocar la infraestructura.

package services

import "myapp/application/usecases"

type UserService struct {
    userUsecase *usecases.UserUsecase
}

func NewUserService(userUsecase *usecases.UserUsecase) *UserService {
    return &UserService{userUsecase: userUsecase}
}

func (s *UserService) FindUsersWithPagination(offset, limit int) ([]domain.User, error) {
    return s.userUsecase.FindWithPagination(offset, limit)
}

📌 Evita lógica de negocio en los controladores o la API.


4. Conclusión

Modularidad clara: Cada capa tiene una única responsabilidad.
Escalabilidad: Se pueden agregar más UseCases y Services sin romper el sistema.
Mantenibilidad: Las capas pueden cambiarse sin afectar a las demás.
Desacoplamiento total: Se pueden cambiar bases de datos o servicios externos sin tocar la lógica de negocio.

Este enfoque ha sido probado en proyectos SaaS y aplicaciones a gran escala, asegurando flexibilidad, velocidad y mantenibilidad.

0
Subscribe to my newsletter

Read articles from Gus directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gus
Gus