Plataforma técnica · Arquitectura
Patrones de Diseño
Arquitectura Patrones
Esta página cataloga los patrones de diseño que el ecosistema aplica de forma deliberada. Cada patrón identifica el problema que resuelve, dónde vive en el código y los servicios o islands que lo utilizan. Los patrones aquí listados son los valores válidos del campo patterns en el frontmatter de páginas kind: service, y se renderizan como chips en el footer de esas páginas.
Patrones canónicos
Section titled “Patrones canónicos”Los siguientes valores son los únicos aceptados en patterns del frontmatter:
| Patrón | Tipo | Resumen |
|---|---|---|
singleton | Creacional | Una sola instancia compartida por proceso. |
builder | Creacional | Construcción paso a paso de objetos complejos con validación incremental. |
factory | Creacional | Centraliza la creación de objetos según un criterio runtime. |
calculator | Creacional | Función pura encapsulada como clase: input estructurado → output estructurado, sin TX, IO ni eventos. |
generator | Creacional | Produce un artefacto final (HTML, PDF, CSV) desde datos de dominio; renderiza, no calcula. |
strategy | Comportamiento | Algoritmos intercambiables tras una interfaz común. |
observer | Comportamiento | Suscripción a eventos sin acoplar emisor y receptor. |
template-method | Comportamiento | Una clase base define el esqueleto del algoritmo; las subclases rellenan los pasos. |
state-machine | Comportamiento | Transiciones de estado explícitas con guards; el estado actual define qué operaciones son legales. |
transactional-outbox | Comportamiento | Encolar eventos durante una TX y emitirlos solo después del COMMIT. |
decorator | Estructural | Envuelve un objeto añadiendo responsabilidades sin modificarlo. |
facade | Estructural | Capa delgada que expone una API estable componiendo o re-exportando subservicios. |
repository | Estructural | Aísla las queries SQL en una clase dedicada; el servicio solo invoca métodos. |
Singleton
Section titled “Singleton”Una sola instancia por proceso, compartida por todos los consumidores. En Orchestrator se usa para recursos costosos de crear y seguros para compartir (pools de conexión, loggers, validadores compilados).
Cuándo aplicarlo: estado compartido inmutable o thread-safe, recursos con costo de inicialización (handshake de DB, carga de JSON), idempotencia garantizada por construcción.
Cuándo NO aplicarlo: cualquier cosa con estado mutable por request (ahí va ctx por handler, no singleton).
Builder
Section titled “Builder”Construcción incremental de un objeto cuando hay muchos campos opcionales o reglas de validación entre ellos. Evita constructores con 10 parámetros y permite que cada paso falle con un mensaje específico.
Cuándo aplicarlo: objetos con más de 5 campos opcionales, validaciones encadenadas (un campo solo válido si otro está presente), construcción que cruza varios servicios.
Ejemplo típico: armar un payload de liquidación de remuneraciones reuniendo contrato, asistencia, haberes, descuentos y previsionales antes de calcular el total.
Factory
Section titled “Factory”Encapsula la decisión de qué tipo concreto crear según un parámetro runtime (régimen tributario, tipo de cuenta, formato de exportación). El llamador pide “dame el calculador para este caso” sin conocer las subclases.
Cuándo aplicarlo: el código tiene un switch por tipo que aparece en varios sitios, agregar un nuevo tipo debería tocar un solo archivo.
Calculator
Section titled “Calculator”Función pura encapsulada como clase: recibe un input estructurado, retorna un output estructurado y no toca el mundo (sin TX, sin queries, sin eventos, sin reloj). La razón de ser una clase y no una función suelta es agrupar reglas relacionadas (constantes, validaciones internas, sub-cálculos privados) sin polucionar el namespace del servicio.
La forma canónica vive en payroll/calculators/ con 7 clases (BaseSalary, Gratification, SocialLaws, Tax, Prorrata, HealthPlan, plus FiniquitoCalculator en finiquitos). PayrollEngine las compone llamándolas en secuencia con el output de una alimentando la siguiente.
Cuándo aplicarlo: una fórmula contable o tributaria tiene varios sub-pasos con valores intermedios, los inputs son explícitos y serializables, el resultado debe ser determinista y testeable sin DB.
Cuándo NO aplicarlo: si el cálculo necesita leer parámetros vigentes desde DB en medio del flujo, esa lectura debe vivir en el service (o pasarse como input al calculator). El calculator no debe abrir conexiones.
Generator
Section titled “Generator”Produce un artefacto final (HTML, PDF, CSV, JSON exportable) desde datos de dominio ya consolidados. La diferencia con builder es que el output es un blob/string para consumo externo, no un objeto de dominio que continuará procesándose.
Casos en el código: ContractGenerator, ContractHtmlGenerator, FiniquitoHtmlGenerator, PdfService, los exportadores CSV de DJ 1879/1887/F29.
Cuándo aplicarlo: el destino del output es un usuario externo (SII, trabajador, contraparte), el formato es rígido (columnas, secciones), la lógica de presentación no debería mezclarse con la lógica de cálculo.
Cuándo NO aplicarlo: si el “artefacto” es un objeto que otra parte del sistema seguirá leyendo y mutando — eso es builder o construcción directa.
Strategy
Section titled “Strategy”Familia de algoritmos intercambiables tras una misma interfaz. La diferencia con Factory es que strategy se inyecta antes y no cambia durante la ejecución; factory decide al momento de crear.
Cuándo aplicarlo: misma operación con varias implementaciones (cálculo de impuesto IDPC vs Pro Pyme), elegir la implementación por configuración o por contexto del tenant.
Observer
Section titled “Observer”Suscripción a eventos para desacoplar quién emite de quién escucha. En Sevastopol se usa para coordinar islands sin acoplarlas (un island dispara sidebar:navigate, otros escuchan). En Orchestrator se usa con menos frecuencia, principalmente en monitoring.
Cuándo aplicarlo: un evento puede tener cero, uno o muchos consumidores; los consumidores no deben afectar al emisor; el orden de notificación no importa.
Template Method
Section titled “Template Method”Una clase base define el orden de los pasos de un algoritmo y delega los pasos variables a las subclases. La subclase no controla el flujo; solo rellena los huecos.
La forma canónica en el código es BaseService (domain/common/BaseService.ts): define withTransaction (BEGIN → callback → COMMIT/ROLLBACK → flushOutbox), withSerializableTransaction (igual + retry en 40001), helpers de validación, logging contextual y result builders. Cada servicio de dominio extiende BaseService y solo escribe la lógica específica dentro de los callbacks.
Cuándo aplicarlo: un flujo se repite en muchas subclases (apertura de TX, manejo de errores, logging) y la única diferencia entre ellas es 2-3 pasos puntuales.
Cuándo NO aplicarlo: si los pasos varían demasiado, la base se llena de hooks vacíos o flags if (typeof this.x === 'function'). Ahí conviene strategy o composición.
State Machine
Section titled “State Machine”Transiciones de estado explícitas con guards: el estado actual del recurso define qué operaciones son legales. El servicio rechaza transiciones inválidas con un error de dominio en vez de mutar silenciosamente.
Aparece de dos formas:
- Explícito:
PermissionServicetiene un mapa de transiciones permitidas y rechaza el resto. - Implícito por convención: la mayoría de los recursos tributarios/operativos lleva un campo
estadocon valores fijos y el service guardia las transiciones enupdateStatus. Ejemplos: F29 (BORRADOR → VALIDADO → DECLARADO/RECTIFICADO/ANULADO), liquidaciones (BORRADOR → CALCULADA → APROBADA → PAGADA), compras (PENDIENTE → CONTABILIZADO → PAGADO), depreciaciones (PENDIENTE → CONTABILIZADO → ANULADO), conciliación bancaria.
Cuándo aplicarlo: el recurso tiene ≥3 estados con reglas de qué transiciones son legales, una transición errónea tiene consecuencias contables (un F29 que retrocede de DECLARADO cuando hay IVA pagado contamina libros), las reglas deben quedar centralizadas y auditables.
Cuándo NO aplicarlo: un booleano activo: true/false no es una máquina de estados. Tampoco lo es un estado que solo cambia en una dirección sin validación.
Transactional Outbox
Section titled “Transactional Outbox”Encolar los eventos a emitir durante una transacción y emitirlos al DomainEventBus solo después del COMMIT. Si la TX rollback, la cola se descarta — los listeners nunca ven el evento.
Implementado en BaseService.withTransaction(ctx, (client, outbox) => ...): el callback recibe un outbox.queue(event, payload); después del COMMIT, flushOutbox enriquece cada payload con tenantDb/userId/requestId y los emite. Detalle en BaseService › Outbox Transaccional.
Cuándo aplicarlo: el servicio muta DB y necesita notificar a otros dominios; los listeners pueden tener efectos que deben verse solo si la mutación persistió.
Cuándo NO aplicarlo: notificación pura para observabilidad (audit log, métricas) que debe verse aunque la TX rollback — usar sinks o emit directo.
Decorator
Section titled “Decorator”Envuelve un objeto añadiendo comportamiento (logging, caching, retry, autorización) sin modificar la clase original. La forma más común en el código son los middleware de Express (authenticateToken, authorizeRoute) y el wrapper de repositories (wrapStaticRepository en domain/common/decorators/RepositoryDecorators.ts).
Cuándo aplicarlo: el comportamiento añadido es ortogonal al dominio (no es regla de negocio), múltiples objetos lo necesitan, debe poder activarse o desactivarse por configuración.
Facade
Section titled “Facade”Capa delgada que expone una API estable componiendo o re-exportando subservicios. La diferencia con un servicio normal es que la fachada no contiene lógica de negocio: solo enruta llamadas, no abre transacciones propias, no emite eventos.
Tres formas en el código:
- Facade de re-export:
InventarioServiceyInventarioRepository(comentario literal “Thin facade that re-exports…”). Sirve para mantener compatibilidad de rutas HTTP existentes mientras la implementación se divide en servicios especializados. - Facade de composición:
CicloContableService.getEstadoagrega 10+ queries de distintos esquemas en un único snapshot read-only sin agregar lógica de negocio propia. - Facade de dominio: cuando un service expone métodos que delegan en sub-services del mismo paquete y la única razón de existir es ergonomía del caller.
Cuándo aplicarlo: un dominio creció a >5 servicios y los callers no deberían conocer la división interna; un dominio existente se divide y hay que mantener la API HTTP estable.
Cuándo NO aplicarlo: si la fachada va a acumular lógica que no es enrutamiento (validación cruzada, transacciones que abarcan varios subservicios), ya no es fachada — conviene refactor a un orquestador con su propio dominio.
Repository
Section titled “Repository”Aísla todo el SQL en una clase dedicada (*Repository); el servicio solo invoca métodos del repository y nunca escribe queries directamente. Permite cambiar el motor de persistencia, mockear en tests y razonar sobre cardinalidad de queries por servicio.
La convención en este código es:
- Clase con métodos estáticos async (e.g.
CapitalRepository.findById(pool, id)). - Recibe un
Pool | PoolClientcomo primer parámetro para participar en TX externas. - Opcionalmente envuelta con
wrapStaticRepositorypara slow-query log y retry (decorator).
Detalle en SQL Builders y RepositoryDecorators.
Cuándo aplicarlo: cualquier servicio que toque PostgreSQL más de una vez. Es el patrón por defecto del proyecto.
Cuándo NO aplicarlo: lecturas one-off triviales (un SELECT 1 en health check) o servicios cuya persistencia no es PostgreSQL — e.g. RegistryService lee un JSON desde disco.
Cómo declarar patterns en una página de servicio
Section titled “Cómo declarar patterns en una página de servicio”En el frontmatter de cualquier página kind: service, patterns va anidado dentro de related: — el componente Related.astro lee desde related.patterns para renderizar los chips en el footer:
related: patterns: - template-method - repository references: - /dev/orchestrator/services/common/baseservice/Solo valores de la tabla canónica. Si un servicio implementa un patrón fuera de la lista, primero discutir si añadirlo aquí o si conviene reescribir el servicio para usar uno existente.