Skip to content

Arquitectura Hexagonal DDD

GM Fiscal Backend implementa Arquitectura Hexagonal (también conocida como Ports & Adapters) combinada con Domain-Driven Design (DDD). Esta combinación garantiza que la lógica de negocio fiscal sea completamente independiente de la infraestructura —base de datos, HTTP, gRPC, Azure— y verificable en tests sin dependencias externas.

La migración incremental del código legado se gestiona mediante el patrón Strangler Fig, que permite reemplazar servicios legados módulo por módulo sin romper el funcionamiento del sistema. El estado actual de migración es la fase §11.11.

Regla de dependencias

La regla fundamental de la arquitectura es que las capas superiores nunca importan capas inferiores. El flujo de dependencias es estrictamente unidireccional:

domain/ ← solo stdlib + domain/shared
^
application/ ← solo domain/*
^
infrastructure/ ← domain/* + application/* + paquetes externos
^
presentation/ ← application/* + domain/shared

Esta restricción se puede verificar en cualquier momento con los siguientes comandos, que deben producir cero resultados:

Terminal window
grep -r "infrastructure" src/domain/
grep -r 'gorm:"' src/domain/
grep -r "infrastructure" src/application/
grep -r "\.db\." src/presentation/

Cualquier resultado positivo en estas verificaciones indica una violación arquitectónica que debe corregirse antes de cualquier merge.

Las cuatro capas

Capa de dominio (src/domain/)

El núcleo puro del negocio. Contiene entidades, interfaces de repositorios (puertos), errores de dominio y servicios de dominio. No tiene dependencias de infraestructura, ORM ni frameworks externos. Las entidades de dominio no son modelos de base de datos y nunca contienen tags gorm:"...".

Esta capa es la de mayor importancia y estabilidad del sistema. Sus contratos —interfaces de repositorio y servicios— son los que guían el diseño de todas las capas superiores.

Capa de aplicación (src/application/)

Contiene los casos de uso. Su responsabilidad es orquestar el flujo de negocio a través de los puertos (interfaces) definidos en el dominio. Nunca accede directamente a infraestructura. Es completamente testeable con mocks sin necesidad de base de datos ni HTTP.

Cada bounded context tiene su propio directorio con la estructura:

src/application/[bc]/
├── use_cases.go # Implementación de los casos de uso
├── use_cases_test.go # Tests TDD con mocks testify
├── ports.go # Interfaces de puertos de aplicación
└── dtos.go # Comandos y queries

Capa de infraestructura (src/infrastructure/)

Implementaciones concretas de los puertos definidos en el dominio y la aplicación. Esta capa es la única que puede importar paquetes externos como GORM, el SDK de Azure, clientes gRPC o el driver HTTP de Prodigia.

infrastructure/
├── persistence/gorm/ # Repositorios GORM por bounded context
├── gateways/
│ ├── erp/ # Cliente HTTP al ERP interno
│ ├── prodigia/ # Cliente HTTP a Prodigia + xml_utils.go
│ ├── hades/ # Cliente gRPC + adaptador caché SAT
│ └── servicios_gm/ # Auth gateway, importa_client
├── adapters/ # Adaptadores Strangler Fig (wrapping legado)
├── storage/ # Azure Blob + fallback local
├── generators/ # R21 + RAMCI: procesadores, Excel, ZIP
├── workers/ # ReporteWorker, ComprobanteDownloadWorker
├── websocket/ # Manager de conexiones WebSocket
└── logging/ # Adaptador gmlog async TCP

Capa de presentación (src/presentation/http/)

Handlers HTTP. Su única responsabilidad es parsear la request entrante, delegar la ejecución al caso de uso correspondiente y formatear la response. No contiene lógica de negocio.

presentation/http/
├── handlers/ # Handlers HTTP por bounded context
├── middleware/ # JWT auth, CORS, request logging
└── routes/ # routes.go — contrato HTTP permanente

Composition Root

Toda la inyección de dependencias ocurre en un único lugar: src/main.go. Es el único punto del sistema donde se instancian repositorios, gateways y casos de uso. No existen singletons globales ni uso de sync.Once para inicialización de dependencias.

El orden de inicialización es determinístico:

flowchart TD
    A[OpenSSL check] --> B[config.LoadConfig]
    B --> C[gmlog.NewClient]
    C --> D[storage.NewAzureStorage]
    D --> E[database.Connect PostgreSQL 5433]
    E --> F[database.InitGorm auto-migrate]
    F --> G[Wiring: repos + gateways + use cases + handlers]
    G --> H[routes.SetupRoutes]
    H --> I[workers.Start: ReporteWorker + ComprobanteDownloadWorker]
    I --> J[http.ListenAndServe :8046]
    J --> K[Graceful shutdown]

La función config.LoadConfig() está prohibida en cualquier lugar que no sea main.go. Esta regla evita que la configuración sea accedida en distintos momentos del ciclo de vida y garantiza que el sistema falle de forma explícita al arranque si faltan variables de entorno.

Patrón Strangler Fig

El código legado en los directorios controller/, handlers/, services/ y models/ está siendo migrado incrementalmente. El proceso de tombstonear un servicio legado sigue cinco pasos:

  1. Crear el port (interface) en application/ o domain/.
  2. Crear el adapter en infrastructure/adapters/[bc]_adapter.go.
  3. El handler recibe el port, no el servicio legado concreto.
  4. Marcar el legado con el comentario de tombstone.
  5. Eliminar el legado en la siguiente iteración.
// §DDD Strangler Fig — TOMBSTONEADO.
// XxxService → infrastructure/adapters/xxx_adapter.go

La regla de granularidad es estricta: un commit por bounded context. Nunca agrupar la migración de varios dominios en un solo commit.

Modelo de datos

Los modelos GORM con tags gorm:"..." residen exclusivamente en src/models/. Las entidades de dominio en src/domain/[bc]/entity.go son estructuras limpias sin anotaciones de infraestructura. Esta separación evita que las necesidades de persistencia contaminen el modelo de dominio.

TablaModelo GoPropósito
empresasmodels.EmpresaCompañías registradas
solicitudesmodels.SolicitudSolicitudes SAT
conciliacionesmodels.ConciliacionResultados de conciliación
conciliacion_facturasmodels.ConciliacionFacturaFacturas por conciliación
reportesmodels.ReporteR21/RAMCI generados
notificationsmodels.NotificationNotificaciones WebSocket
comprobante_downloadsmodels.ComprobanteDownloadCola de descarga
sesiones_usuariomodels.SesionUsuarioSesiones JWT
request_logsmodels.RequestLogAuditoría HTTP
cfdi_cache_entriesmodels.CFDICacheEntryCaché CFDI (PG 5434 + Azure)
erp_cache_entriesmodels.ERPCacheEntryCaché consultas ERP
solicitud_assetsmodels.SolicitudAssetAssets pre-conciliación
xmlsmodels.XMLXMLs almacenados en BD

Flujo de desarrollo Outside-In

Todo nuevo funcionalidad sigue el protocolo Outside-In en un orden estricto y sin posibilidad de saltarse pasos:

  1. BDD — Escenario Gherkin en español antes de cualquier código de implementación. El test debe fallar primero.
  2. TDD — Test del caso de uso con mocks (RED → GREEN). Nomenclatura: TestNombreUC_Condicion_Resultado.
  3. UNIT — Tests de funciones puras de dominio. Table-driven para múltiples casos. Sin mocks.
  4. DDD Hexagonal — Implementación en orden: domain → mocks → infrastructure → adapter → presentation.
  5. main.go — Solo con confirmación explícita. Registrar el nuevo use case en el composition root.

La verificación final antes de cualquier PR incluye:

Terminal window
go vet ./src/...
go build ./src
go test ./src/domain/...
go test ./src/application/...
go test ./tests/...
go test ./... -race -count=1

Resumen

  • La arquitectura hexagonal DDD garantiza que el dominio fiscal sea independiente de infraestructura y verificable en tests sin dependencias externas.
  • El flujo de dependencias es unidireccional: domain ← application ← infrastructure ← presentation. Ninguna excepción.
  • Toda la inyección de dependencias ocurre en src/main.go como único Composition Root.
  • El patrón Strangler Fig gestiona la migración incremental del legado: un bounded context por commit.
  • El protocolo Outside-In (BDD → TDD → UNIT → DDD → main) es obligatorio para toda funcionalidad nueva.