Appearance
Arquitectura de la Aplicación Ninaku (ARCHITECTURE.md)
Estado: Línea base arquitectónica normativa para ninaku-app-api.
Este documento define la arquitectura de aplicación objetivo para Ninaku sobre NestJS. Conserva las fronteras de dominio y los contratos de base de datos validados en la línea base canónica de PostgreSQL, adaptando la estructura de la aplicación a TypeScript / NestJS.
El objetivo no es dividir Ninaku en microservicios, sino construir una sola aplicación desplegable con fronteras internas fuertes.
1. Estilo Arquitectónico
Ninaku es un monolito modular con Clean Architecture y DDD (Domain-Driven Design) pragmático por módulo.
- Una sola aplicación backend.
- Una sola unidad de despliegue por defecto.
- Una sola base de datos PostgreSQL de plataforma por defecto.
- Módulos de negocio explícitos con propiedad clara de datos y lógica.
- Prohibido realizar llamadas HTTP internas entre módulos dentro del mismo proceso.
- Sin "servicio Dios" compartido ni repositorios genéricos que abarquen todos los dominios.
- Los módulos colaboran exclusivamente a través de contratos públicos explícitos en proceso.
- Las verticales de producto componen capacidades reutilizables de la plataforma en lugar de duplicarlas.
La dirección de dependencia es conceptual: las verticales consumen capacidades reutilizables; las capacidades reutilizables consumen el core de la plataforma y la infraestructura transversal. Una vertical nunca debe convertirse en una puerta trasera para mutar datos arbitrarios pertenecientes a otro módulo.
2. Modelo de Producto
Ninaku distingue explícitamente estos conceptos:
- Organization: Límite tenant / cliente.
- Business Unit: Subdivisión operativa y administrativa dentro de una organización.
- Legal Entity: Límite de propiedad legal y fiscal (RUC / RIF / NIF).
- Brand: Identidad comercial.
- Site: Grupo de ubicaciones físicas o lógicas.
- Outlet: Punto de venta u operación física/digital donde ocurre la actividad comercial.
- Vertical: Familia de capacidades funcionales de producto (Restaurante, Retail, Hospitalidad).
Una vertical no es un tenant, una Business Unit, una Legal Entity ni un Outlet.
Una organización puede activar distintas verticales para diferentes Business Units. Esto es lo que permite que un grupo hotelero, por ejemplo, ejecute capacidades de hospitalidad y restaurante/alimentos en la misma plataforma Ninaku sin crear sistemas desconectados.
3. Principio Central: Un Hecho de Negocio, Un Dueño
Cada hecho de negocio relevante tiene un único módulo propietario:
salesposee las ventas, líneas de venta, ciclo de vida de la orden y devoluciones.paymentsposee intenciones de pago, intentos, resultados, reversos y reembolsos.cashposee la custodia de efectivo, turnos/cajas, conteos y diferencias.treasuryposee cuentas bancarias/wallets, liquidaciones, tipos de cambio y conciliación.inventoryposee la custodia de mercancía, lotes, reservas, movimientos y conteos.accountingposee libros contables, solicitudes de contabilización y asientos.
Esto significa que:
- Ventas no es Pagos.
- Pagos no es Caja.
- Pagos no es Tesorería.
- Catálogo no es Inventario.
- Catálogo no es Producción.
- Impuestos no es Fiscal.
- Fiscal no es Facturación.
- Servicio de Restaurante no es Reservas Hoteleras.
Una acción del usuario puede desencadenar efectos en varios módulos, pero eso no fusiona la propiedad del dato ni rompe las fronteras de los agregados.
4. Disposición del Código Fuente en src/
El código fuente de la aplicación evoluciona hacia la siguiente disposición canónica de carpetas:
text
src/
├── app/
│ └── app.module.ts
│
├── bootstrap/
│ ├── configure-http-application.ts
│ ├── create-application.ts
│ └── graceful-shutdown.ts
│
├── foundation/
│ ├── config/
│ ├── correlation/
│ ├── database/
│ ├── errors/
│ ├── events/
│ ├── http/
│ ├── idempotency/
│ ├── logging/
│ ├── tenancy/
│ ├── transactions/
│ └── validation/
│
├── modules/
│ ├── reference/
│ ├── organization/
│ ├── party/
│ ├── identity/
│ ├── platform/
│ ├── assets/
│ ├── catalog/
│ ├── pricing/
│ ├── commerce/
│ ├── sales/
│ ├── settlement/
│ ├── payments/
│ ├── inventory/
│ ├── production/
│ ├── procurement/
│ ├── crm/
│ ├── loyalty/
│ ├── receivables/
│ ├── payables/
│ ├── cash/
│ ├── treasury/
│ ├── tax/
│ ├── fiscal/
│ ├── invoicing/
│ ├── accounting/
│ ├── workforce/
│ ├── documents/
│ ├── printing/
│ ├── notifications/
│ ├── communications/
│ ├── integrations/
│ ├── events/
│ ├── sync/
│ ├── audit/
│ └── analytics/
│
├── verticals/
│ ├── restaurant/
│ │ ├── service/
│ │ └── kitchen/
│ ├── retail/
│ └── hospitality/
│
└── main.tsEsta estructura representa el mapa objetivo. Una carpeta existe porque existe una responsabilidad en código, no para llenar diagramas vacíos.
El directorio bootstrap/ posee la secuencia de arranque: construir la aplicación NestJS, aplicar políticas HTTP y orquestar el apagado (graceful shutdown). main.ts permanece en la raíz de src/ como el punto de entrada del proceso ejecutado por Node.js y nombrado en package.json y .railway/railway.ts.
5. Foundation contra Módulos de Negocio, y Módulos Ambient contra Capability
La carpeta foundation/ contiene las capacidades técnicas transversales compartidas por toda la aplicación. Nunca debe poseer conceptos de negocio como ventas, restaurantes, inventario o contabilidad.
Las capacidades típicas de foundation/ incluyen:
- Administrador de transacciones y contexto de tenant (
AsyncLocalStorage). - Conexión y ciclo de vida de la base de datos PostgreSQL.
- Traducción y catálogo de errores estandarizados (RFC 9457).
- Registro estructurado de logs y trazabilidad distribuida W3C / OpenTelemetry.
- Infraestructura de validación (Zod / Standard Schema) y filtros HTTP.
- Deduplicación e idempotencia de comandos.
Clasificación de Módulos de Foundation
Los módulos de Foundation se dividen en dos categorías según su alcance:
- Módulos Ambientales (
@Global()): Módulos ambientales globales que toda la aplicación requiere de fondo. Declarar una importación explícita solo reafirmaría lo que ya se asume:FoundationConfigModule: Configuración tipada del entorno.LoggingModule: Registro JSON estructurado y redacción de secretos.ObservabilityModule: Correlación de peticiones y trazas.
- Módulos de Capacidad (
Capability Modules, Importación Explícita): Módulos de infraestructura que un consumidor usa o no usa. Su inclusión debe ser visible en el arregloimports: [...]de cada consumidor:DatabaseModule: Fachadas transaccionales y salud de PostgreSQL.CollectionsModule: Codificación y firma de cursores opacos.TelemetryModule: Métricas HTTP.
Razón del diseño: La explicitud revela la propiedad. Si DatabaseModule fuera @Global(), no se podría saber qué módulos acceden a PostgreSQL leyendo sus importaciones. Las pruebas unitarias de los módulos de capacidad certifican que Nest rehusará resolver la dependencia si la importación explícita se omite.
6. Estructura Interna de un Módulo
Un módulo de negocio en src/modules/<modulo>/ utiliza la siguiente estructura interna por capas:
text
src/modules/identity/
├── domain/ <-- Reglas de negocio puras y agregados
├── application/ <-- Casos de uso y puertos
│ └── ports/ <-- Interfases requeridas por la aplicación
├── infrastructure/ <-- Adaptadores de persistencia y servicios
├── presentation/ <-- Controladores HTTP, DTOs y catálogo de fallos
│ ├── controllers/
│ ├── dtos/
│ └── failures/
├── contracts/ <-- Superficie pública in-process para otros módulos
└── identity.module.tsContenido y Responsabilidad por Carpeta:
domain/: Reglas puras de negocio, entidades, objetos de valor y eventos de dominio. No tiene dependencias con NestJS, HTTP, drivers de PostgreSQL, Zod u ORMs. Los nombres de carpetas y archivos responden al lenguaje ubicuo del dominio (ej.identifier/,credentials/), nunca a términos técnicos (helpers/,types/).application/: Contiene los casos de uso (orquestación del workflow) y la carpetaports/.ports/: Define las interfaces que el módulo necesita de la infraestructura (ej.PasswordHasherPort,IdentitySessionRepositoryPort).- Casos de uso: Nombrados estrictamente con el patrón
<verb>-<subject>.use-case.tsexponiendo un solo métodoexecute(...). Todo valor o hash requerido debe generarse antes de abrir la transacción SQL para evitar efectos secundarios ante reintentos por contención.
infrastructure/: Implementa los adaptadores técnicos para PostgreSQL, hashing, tokens JWT, mensajería y almacenamiento, satisfaciendo los puertos declarados enapplication/ports/.presentation/: Adaptador para el protocolo HTTP.controllers/: Controladores HTTP delgados. Validan la entrada con esquemas Zod, llaman a un solo caso de uso y proyectan el DTO. Cerotry/catch.dtos/: Esquemas Zod strictly configurados (.strict()) y tipos derivados (z.infer).failures/: CatálogoPublicFailuredel módulo, el mapeadortoPublicFailurey elNestInterceptorque traduce las excepciones de dominio antes de que lleguen alProblemDetailsFilterglobal. Es un interceptor y no unExceptionFilterde módulo porque ese filtro sería terminal: al relanzar no alcanzaría al filtro global y todo fallo de dominio contestaría500.
contracts/: Superficie pública in-process que otros módulos pueden consumir. Define las clases abstractas e interfaces DTO del contrato.
7. Colaboración entre Módulos
Un módulo de negocio escribe y gestiona sus propios datos. Ningún módulo tiene permitido acceder directamente a los repositorios, modelos de base de datos o SQL de otro módulo.
ts
// ❌ RECHAZADO: Acceso directo a repositorios ajenos
class ConfirmSaleUseCase {
constructor(
private readonly inventoryRepository: InventoryRepository,
private readonly paymentRepository: PaymentRepository,
) {}
}
// ✅ PREFERIDO: Colaboración mediante Contratos Públicos
class ConfirmSaleUseCase {
constructor(
private readonly inventory: InventoryContract,
private readonly payments: PaymentsContract,
) {}
}La colaboración síncrona en proceso se realiza invocando métodos de la clase abstracta expuesta en contracts/. No existen llamadas HTTP internas entre módulos del mismo proceso.
8. Diseño de API
Las APIs de Ninaku se diseñan alrededor de tareas de negocio y flujos de pantalla, no reflejando tablas de base de datos.
text
✅ Endpoints Orientados a Tareas:
POST /api/v1/sales/orders
POST /api/v1/sales/orders/{id}/confirm
POST /api/v1/inventory/transfers
POST /api/v1/inventory/transfers/{id}/dispatch
POST /api/v1/payments/intents
POST /api/v1/payments/{id}/captureSe evita la creación de endpoints CRUD automáticos por tabla. Para la carga de pantallas complejas, un compositor de consultas de lectura puede combinar DTOs de lectura de múltiples contratos públicos en una sola respuesta, manteniendo las escrituras encapsuladas en casos de uso específicos.
9. Estrategia de Base de Datos
La base de datos canónica PostgreSQL bajo database/ es un contrato de plataforma y no se regenera automáticamente desde modelos de la aplicación.
- PostgreSQL 18 es la fuente de verdad inmutable para el esquema canónico.
ninaku_migratorposee los permisos de administración para ejecutar migraciones y scripts DDL.ninaku_runtimees el rol exclusivo y restringido que utiliza la aplicación NestJS. Prohibido ejecutar la API como superusuario o propietario.- Row Level Security (RLS) permanece activa en runtime para garantizar el aislamiento multi-tenant a nivel de motor SQL.
- El esquema SQL no es propiedad de un ORM. Las herramientas de la aplicación se adaptan al esquema PostgreSQL existente.
10. Transacciones entre Módulos
Cuando un caso de uso requiere atomicidad a través de múltiples propietarios de negocio (ej. confirmar una venta debe actualizar sales, reservar inventory e insertar en outbox atómicamente), la operación comparte una misma transacción PostgreSQL.
Reglas de Transacciones:
- La transacción la abre
FoundationmedianteTenantTransactionRunnerService. - Los módulos participantes reciben un contexto de transacción opaco (
TransactionContext). - Ningún contrato expone
pg.PoolClientni manipuladores SQL crudos hacia el caso de uso. - Las llamadas a red o servicios externos nunca se ejecutan dentro de la transacción SQL; utilizan el patrón Outbox/Inbox para garantizar reintentos e idempotencia.
11. Idempotencia y Concurrencia
La idempotencia es obligatoria en comandos donde la ejecución repetida produciría efectos duplicados (confirmar venta, cobrar, despachar stock, reconciliar pago, replay offline).
Mecanismos de Concurrencia por Invariante:
- ETags /
If-Match: Para edición concurrente de borradores y configuraciones (optimistic locking). - Bloqueo Pesimista (
FOR UPDATE): Para reservas de stock y consumo de saldos finitos. - Transiciones Condicionales de Estado: Para confirmaciones y cierres.
- Restricciones
UNIQUEen PostgreSQL: Para garantizar unicidad de efectos atómicos medianteresolveIdempotentClaim.
Prohibido el uso de mutexes en memoria como garantía distribuida entre réplicas.
12. Comportamiento Offline-First y Sincronización
La ejecución offline y online convergen exactamente en el mismo caso de uso de la aplicación NestJS.
El cliente local (dispositivo móvil/escritorio) guarda atómicamente el comando y su estado local en SQLite. Al reconectar, envía el comando usando un commandId e Idempotency-Key estables. El módulo sync administra el transporte y el ordenamiento de mensajes, pero no duplica ni reimplementa las reglas de negocio de sales, inventory o payments.
13. Verticales
Las verticales contienen capacidades de producto específicas para una familia funcional:
- Restaurant: Áreas de atención, mesas, sesiones de servicio, comandas y estaciones de cocina (
kitchen). - Hospitality: Propiedades, tipos de habitación, reservas de hospedaje, estadías, folios y housekeeping.
- Retail: Catálogo comercial extendido, código de barras, variantes y atención directa de caja sin mesa ni cocina.
Las verticales son consumidoras de los módulos compartidos (catalog, sales, payments, inventory, printing). La cocina o las mesas de restaurante no son infraestructura universal del core.
14. Ejemplo de Composición Multi-Negocio
Una sola organización puede operar múltiples modelos de negocio bajo el mismo tenant:
text
Organization (Tenant)
├── Business Unit: Hospedaje (Hospitality)
│ └── Operaciones del Hotel
└── Business Unit: Alimentos y Bebidas (Food & Beverage)
├── Restaurante
├── Bar
└── Room ServiceCada Business Unit activa únicamente las verticales que requiere, compartiendo el mismo tenant, catálogo base, entidades legales y tesorería sin contaminar el código con condicionales if (vertical === 'restaurant').
15. Módulos Transversales
Los módulos transversales (events, sync, audit, analytics, printing, notifications, communications, integrations) deben permanecer desacoplados.
- No convierten a todos los módulos de negocio en dependencias obligatorias.
- Utilizan adaptadores y puentes tipados cuando dos módulos necesitan colaborar (ej. un puente
restaurant-printingque conecta eventos de cocina con el servicio de impresión). - Prohibido crear tablas o servicios polimórficos universales que permitan mutar cualquier tabla del sistema.
16. Orden Inicial de Implementación
Antes de desarrollar funciones comerciales o de verticales, se establece la fundación de la aplicación y sus límites modulares en este orden:
- Configuración tipada y arranque de aplicación.
- Conexión a PostgreSQL runtime con rol
ninaku_runtime. - Propagación de contexto de petición y tenant RLS (
AsyncLocalStorage). - Abstracción del administrador de transacciones (
TenantTransactionRunnerService). - Contrato HTTP de error RFC 9457 y filtro de excepciones.
- Infraestructura de idempotencia (
resolveIdempotentClaim). - Infraestructura de eventos de integración y Outbox.
- Convención y reglas de linter para contratos públicos de módulo (
MODULE_CONTRACTS.md). - Módulos de topología (
organization,platform). - Módulo
identity(autenticación, sesiones, membresías). - Onboarding de organizaciones.
- Módulos comerciales compartidos (
catalog,pricing,sales,payments,inventory). - Módulos de verticales (
restaurant,retail,hospitality).
17. No-Objetivos
La arquitectura de Ninaku NO requiere ni busca:
- Un microservicio por cada módulo.
- Una base de datos independiente por cada módulo.
- Una aplicación NestJS independiente por vertical.
- Comunicación HTTP o RPC interna entre módulos dentro del mismo proceso.
- Un esquema de base de datos generado o administrado por un ORM.
- APIs CRUD genéricas orientadas a tablas.
- Duplicar capacidades de la plataforma dentro de cada vertical.
18. Regla Práctica de Arquitectura
Ante cualquier duda sobre la ubicación o diseño de una pieza de código, responde a estas 5 preguntas:
- ¿Quién es el dueño inmutable de este hecho de negocio?
- ¿Es este concepto reutilizable entre verticales o genuinamente específico de una vertical?
- ¿Necesita este módulo un contrato público explícito en
contracts/, o estoy accediendo a la infraestructura interna de otro módulo? - ¿Pertenece esta regla al Dominio (
domain/), la orquestación a Aplicación (application/), la traducción a Presentación (presentation/) o el adaptador a Infraestructura (infrastructure/)? - ¿Preserva esta operación el aislamiento multi-tenant, la idempotencia y las garantías transaccionales?
Si la propiedad no es clara, no la ocultes en common/ o shared/. Resuelve primero la frontera del dominio.