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

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últiplesusecases/
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.
Subscribe to my newsletter
Read articles from Gus directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
