Skip to content

Plataforma técnica · Arquitectura

Arquitectura Hexagonal

Arquitectura Patrones

Esta página explica cómo el sistema aplica el patrón de arquitectura hexagonal (puertos y adaptadores) para mantener el dominio central aislado de la infraestructura. Los flujos completos (BFF, autenticación, multi-tenant) viven en la página de Arquitectura del Sistema.

La arquitectura hexagonal organiza el código en tres capas concéntricas:

  • Dominio central: servicios, motores de cálculo y modelos. Reglas de negocio puras.
  • Puertos: interfaces que el dominio declara para hablar con el mundo exterior.
  • Adaptadores: implementaciones concretas de esos puertos (Express, PostgreSQL, sistema de archivos, scrapers).

El dominio nunca importa código de los adaptadores. Los adaptadores implementan los puertos del dominio. Esto permite reemplazar la infraestructura (cambiar Express por Fastify, PostgreSQL por otra base, etc.) sin tocar las reglas de negocio.

Las reglas que mantienen aislado al dominio:

  • Sin importaciones de tipos express en la lógica de dominio.
  • Sin consultas directas a base de datos en los motores de cálculo (PayrollEngine, TaxCalculator, etc.).
  • Sin operaciones del sistema de archivos en las reglas de negocio.
  • Los servicios reciben dependencias por argumento (Pool, Logger), no las construyen internamente.

flowchart

subgraph EX["Sistemas Externos"]
  WEB["Navegador Web"]
  DB["Base de Datos PostgreSQL"]
  FS["Sistema de Archivos<br/>(Almacenamiento PDF)"]
end

subgraph AD["Adaptadores"]
  FE["Adaptador Frontend (Proxy)<br/>Sevastopol"]
  API["Adaptador API (Rutas)<br/>Express"]
  DB_AD["Adaptador Base de Datos<br/>(Patrón Repository)"]
  FILE_AD["Adaptador de Archivos<br/>(PdfService)"]
end

subgraph PORTS["Puertos (Interfaces)"]
  HTTP_PORT["Puerto de Petición HTTP"]
  DATA_PORT["Puerto de Acceso a Datos"]
  STORAGE_PORT["Puerto de Almacenamiento"]
end

subgraph DOMAIN["Dominio Central"]
  SERVICES["Servicios de Dominio<br/>PayrollService<br/>EmployeeService<br/>ContractService"]
  CALC["Motores de Cálculo<br/>PayrollEngine<br/>TaxCalculator<br/>SocialLawsCalculator"]
  MODELS["Modelos de Dominio<br/>Employee<br/>Contract<br/>Payroll"]
end

WEB -->|HTTP| FE
FE -->|Proxy| API
API -->|Transformar| HTTP_PORT
HTTP_PORT --> SERVICES

SERVICES -->|Usa| CALC
SERVICES -->|Usa| MODELS

SERVICES -->|Consultar / Persistir| DATA_PORT
SERVICES -->|Generar PDF| STORAGE_PORT

DATA_PORT --> DB_AD
STORAGE_PORT --> FILE_AD

DB_AD --> DB
FILE_AD --> FS

PuertoImplementado porQué representa
Petición HTTPRutas Express + middlewareEntrada de comandos del mundo exterior (REST) al dominio.
Acceso a Datos*Repository con pg.PoolLectura y persistencia de entidades de dominio.
AlmacenamientoPdfService con puppeteerGeneración y guardado de documentos (liquidaciones, finiquitos).
Tiempo / IndicadoresCommonDataServiceParámetros temporales (UF, USD, AFP) a la fecha de cálculo.

Cada puerto es una “interfaz de uso” del dominio: el Service lo invoca; el adaptador decide cómo se cumple.


Sevastopol actúa como adaptador HTTP del lado del cliente. No habla con la base de datos: traduce eventos de UI (clicks, formularios) en peticiones HTTP que viajan al adaptador API del Orchestrator, vía un proxy local.

Las rutas en orchestrator/src/routes/ traducen HTTP en llamadas al dominio. Su responsabilidad es:

  • Parsear request (params, body, cookies).
  • Verificar autenticación y autorización.
  • Llamar al servicio de dominio correspondiente.
  • Serializar la respuesta a JSON snake_case.
router.post("/generar", authenticateToken, async (req, res) => {
const pool = getTenantPool(await getDatabaseNameForUser(req.user, req));
const result = await PayrollService.generatePayroll(pool, req.body);
res.json(result);
});

Cada *Repository encapsula las consultas SQL contra PostgreSQL. El dominio no sabe que existe pg: recibe métodos como PayrollRepository.getPayrollContext(pool, input) que devuelven objetos del dominio.

PdfService genera PDFs con Puppeteer y los guarda en LIQUIDACIONES_DIR. El dominio invoca PdfService.generatePayrollPdf(result) sin saber qué motor de renderizado se usa.


Cómo se traduce a las capas del Orchestrator

Section titled “Cómo se traduce a las capas del Orchestrator”
Capa hexagonalCarpeta del OrchestratorEjemplo
Adaptador APIsrc/routes/payroll.ts, employees.ts
Adaptador Repositorysrc/domain/<contexto>/PayrollRepository.ts
Adaptador Almacenamientosrc/domain/common/PdfService.ts
Servicio de Dominiosrc/domain/<contexto>/PayrollService.ts
Motor de Cálculosrc/domain/<contexto>/PayrollEngine.ts, TaxCalculator.ts
Modelossrc/domain/<contexto>/typesPayrollInput, PayrollResult

  • Tests unitarios sin base de datos: el PayrollEngine se prueba con inputs puros, sin Pool ni mocks complejos.
  • Reemplazo de infraestructura aislado: cambiar Puppeteer por otro generador de PDF solo afecta a PdfService.
  • Razón de cambio única por archivo: un bug en cálculo de impuestos vive en TaxCalculator, no en una ruta Express.
  • Onboarding más simple: un desarrollador nuevo puede leer el dominio sin entender Express ni el cliente PostgreSQL.