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ónLa 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 frameworktype 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 dominiotype 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 frameworkexport 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 frameworkclass 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 — Gotype 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 — Gotype 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 — Gofunc (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 rutasNestJS
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.tsFlutter/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.