Skip to content

Clean Architecture

Clean Architecture es el modelo de organización de capas propuesto por Robert C. Martin que unifica conceptos de la Arquitectura Hexagonal, la Onion Architecture y el diseño orientado al dominio en una estructura de cuatro capas con una regla única: las dependencias solo apuntan hacia adentro. Nunca hacia afuera.

En GM Transport, Clean Architecture define la estructura interna de cada servicio. Go (GM Fiscal), NestJS (servicios web) y Flutter/Dart (apps móviles) todos siguen la misma distribución de capas, adaptada a las convenciones propias de cada ecosistema.

La regla de dependencias

La regla fundamental es que ningún código de una capa interna puede conocer nada de una capa externa. El nombre de una función, una clase, una variable o cualquier elemento de software de las capas externas no puede aparecer en el código de las capas internas.

Dominio ← Aplicación ← Infraestructura ← Presentación

La flecha indica la dirección de la dependencia: Aplicación depende de Dominio, no al revés. Infraestructura depende de Aplicación y Dominio. Presentación depende de todo lo demás, pero nada depende de Presentación.

graph TD
    subgraph CA["Clean Architecture"]
        DOM["Dominio\n(Entidades, Puertos, Errores)"]
        APP["Aplicación\n(Casos de Uso, DTOs)"]
        INFRA["Infraestructura\n(Repos GORM, Gateways HTTP, Storage)"]
        PRES["Presentación\n(Handlers HTTP, Middleware, WebSocket)"]
    end

    PRES --> INFRA
    PRES --> APP
    INFRA --> APP
    INFRA --> DOM
    APP --> DOM

Las cuatro capas

Capa de Dominio

El dominio es el núcleo del sistema. Contiene las entidades del negocio, los value objects, las interfaces (puertos) que definen qué necesita el dominio del exterior, los errores de dominio y los servicios de dominio.

El dominio no importa ningún paquete de infraestructura. No sabe qué base de datos existe. No sabe si hay un servidor HTTP. No sabe si la aplicación corre en Go, en NestJS o en Flutter.

// src/domain/solicitud/solicitud.go — Go
// Sin imports de gorm, sql, http, ni ningún framework
type Solicitud struct {
UUID string
RFC string
Estatus EstatusSolicitud
Periodo Periodo
}
func (s *Solicitud) IniciarProcesamiento() error {
if s.Estatus != ListaParaConciliar {
return ErrTransicionInvalida(s.Estatus, EnProceso)
}
s.Estatus = EnProceso
return nil
}
// Puerto definido por el dominio
type SolicitudRepository interface {
FindByUUID(uuid string) (Solicitud, error)
Save(s Solicitud) error
}
// src/domain/solicitud/solicitud.entity.ts — NestJS
// Sin imports de TypeORM, Express ni ningún framework
export class Solicitud {
readonly uuid: string;
private _estatus: EstatusSolicitud;
iniciarProcesamiento(): void {
if (this._estatus !== EstatusSolicitud.ListaParaConciliar) {
throw new InvalidTransitionError(
this._estatus,
EstatusSolicitud.EnProceso,
);
}
this._estatus = EstatusSolicitud.EnProceso;
}
}
// lib/domain/solicitud/solicitud.dart — Flutter/Dart
// Sin imports de Dio, sqflite ni ningún framework
class Solicitud {
final String uuid;
final EstatusSolicitud estatus;
Either<DomainFailure, Solicitud> iniciarProcesamiento() {
if (estatus != EstatusSolicitud.listaParaConciliar) {
return Left(InvalidTransitionFailure(estatus));
}
return Right(copyWith(estatus: EstatusSolicitud.enProceso));
}
}

Capa de Aplicación

La capa de aplicación contiene los casos de uso: cada acción de negocio que el sistema puede realizar. Un caso de uso orquesta entidades de dominio y repositorios para completar una operación. No contiene lógica de negocio —esa vive en el dominio— ni lógica de infraestructura —esa vive en infraestructura.

// src/application/solicitud/conciliar_solicitud.go — Go
type ConciliarSolicitudUseCase struct {
solicitudes domain.SolicitudRepository
conciliaciones domain.ConciliacionRepository
erpGateway domain.ERPGateway
}
func (uc *ConciliarSolicitudUseCase) Execute(cmd ConciliarCommand) (ConciliarResult, error) {
sol, err := uc.solicitudes.FindByUUID(cmd.UUID)
if err != nil {
return ConciliarResult{}, err
}
if err := sol.IniciarProcesamiento(); err != nil {
return ConciliarResult{}, err
}
facturas, err := uc.erpGateway.ObtenerFacturas(sol.RFC, sol.Periodo)
if err != nil {
return ConciliarResult{}, err
}
conciliacion := domain.MakeConciliacion(sol, facturas)
if err := uc.conciliaciones.Save(conciliacion); err != nil {
return ConciliarResult{}, err
}
if err := uc.solicitudes.Save(sol); err != nil {
return ConciliarResult{}, err
}
return ConciliarResult{ConciliacionUUID: conciliacion.UUID}, nil
}

Capa de Infraestructura

La infraestructura implementa los puertos definidos por el dominio usando tecnologías concretas: GORM, TypeORM, SQLAlchemy, Dio, REST, gRPC, Azure Blob Storage, Redis.

// src/infrastructure/persistence/gorm/solicitud_repo.go — Go
type GormSolicitudRepository struct {
db *gorm.DB
}
func (r *GormSolicitudRepository) FindByUUID(uuid string) (solicitud.Solicitud, error) {
var model SolicitudModel
if err := r.db.Where("uuid = ?", uuid).First(&model).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return solicitud.Solicitud{}, solicitud.ErrNotFound(uuid)
}
return solicitud.Solicitud{}, err
}
return toDomain(model), nil
}

Capa de Presentación

La presentación maneja la comunicación con el exterior: endpoints HTTP, WebSockets, colas de mensajes, comandos CLI. Transforma las peticiones externas en comandos de casos de uso y los resultados en respuestas apropiadas para cada protocolo.

// src/presentation/http/handlers/solicitud_handler.go — Go
func (h *SolicitudHandler) ConciliarSolicitud(w http.ResponseWriter, r *http.Request) {
uuid := chi.URLParam(r, "uuid")
result, err := h.conciliarUC.Execute(solicitud_app.ConciliarCommand{UUID: uuid})
if err != nil {
h.responder.Error(w, err)
return
}
h.responder.JSON(w, http.StatusAccepted, result)
}

Estructura de directorios por tecnología

Go

src/
├── domain/
│ ├── solicitud/
│ │ ├── solicitud.go # Entidad + reglas de negocio
│ │ ├── repository.go # Puerto (interfaz)
│ │ └── errors.go # Errores de dominio
│ └── shared/
├── application/
│ └── solicitud/
│ ├── conciliar.go # Caso de uso
│ └── dto.go # DTOs de entrada/salida
├── infrastructure/
│ ├── persistence/gorm/ # Repositorios GORM
│ ├── gateways/ # Clientes externos (Hades, ERP)
│ └── storage/ # Azure Blob Storage
└── presentation/
└── http/
├── handlers/ # Handlers HTTP
├── middleware/ # Auth, logging, CORS
└── router.go # Registro de rutas

NestJS

src/
├── domain/
│ └── solicitud/
│ ├── solicitud.entity.ts
│ └── solicitud.repository.ts # Interfaz
├── application/
│ └── solicitud/
│ └── conciliar-solicitud.use-case.ts
├── infrastructure/
│ └── persistence/typeorm/
│ └── solicitud.typeorm-repository.ts
└── presentation/
└── http/
└── solicitud.controller.ts

Flutter/Dart

lib/
├── domain/
│ └── solicitud/
│ ├── solicitud.dart # Entidad
│ └── solicitud_repository.dart # Interfaz abstracta
├── application/
│ └── solicitud/
│ └── conciliar_use_case.dart
├── infrastructure/
│ └── solicitud/
│ └── solicitud_http_repository.dart
└── presentation/
└── solicitud/
├── solicitud_page.dart
└── solicitud_notifier.dart # Estado (Riverpod)

Resumen

  • Clean Architecture organiza el código en cuatro capas: Dominio, Aplicación, Infraestructura y Presentación. Las dependencias apuntan exclusivamente hacia el dominio.
  • El Dominio nunca importa infraestructura. Define interfaces (puertos) que la infraestructura implementa.
  • La Aplicación orquesta casos de uso coordinando entidades de dominio y repositorios. No contiene lógica de negocio ni de infraestructura.
  • La Infraestructura implementa los puertos del dominio con tecnologías concretas: GORM, TypeORM, Dio, REST, gRPC.
  • La Presentación recibe peticiones externas y las traduce a comandos de casos de uso. Es la única capa que conoce el protocolo de comunicación.
  • El beneficio práctico más directo es la testabilidad: los casos de uso y las entidades se prueban con mocks sin necesidad de levantar infraestructura.