ConTodo ERP — Arquitectura de Software: Bounded Contexts (DDD), Event-Driven, API-First y Patrones
ConTodo ERP — Arquitectura de Software
Propósito. Este entregable consolida y reconcilia las decisiones de arquitectura de software de ConTodo, el ERP SaaS multi-tenant cloud-native para PYMEs y medianas empresas de Perú y LATAM (textiles, importadoras, comercializadoras, distribuidoras y manufactura ligera). A diferencia de los documentos por especialista, este artefacto es vinculante: fija el contrato de bounded contexts (DDD), el modelo event-driven (eventos + outbox), la estrategia API-first (REST + GraphQL + webhooks + API Gateway + marketplace) y los patrones tácticos que el equipo debe respetar. Resuelve además las contradicciones inter-documento que el Partner de Tecnología detectó en la revisión crítica (debate-02): nombre y tipo de
tenant_id, número de buses de eventos, y la frontera entre outbox y CDC.
Anti-overclaiming. Ninguna arquitectura es "la correcta" en abstracto. Las decisiones aquí seleccionan el punto que mejor equilibra el perfil real de ConTodo —muchos tenants PYME pequeños, integridad contable ACID, compliance SUNAT, equipo de 4–6 ingenieros— sobre alternativas igualmente válidas para otros perfiles. Cada decisión declara la alternativa descartada y el disparador que la reabriría.
1. Resumen ejecutivo de decisiones (ADR maestro)
Este documento absorbe el mandato del debate técnico de producir un único contrato de decisiones. Las contradicciones identificadas (C1–C8 del debate-02) se cierran aquí:
| ADR | Decisión vinculante | Alternativa descartada | Disparador de reapertura |
|---|---|---|---|
| ADR-01 | Modular Monolith sobre Rails 7.2, organizado en bounded contexts con packwerk (un pack por context). | Microservicios desde día 1 (sagas distribuidas, latencia, complejidad 5x). | Un context con perfil de escala radicalmente distinto (ej. IA/forecast) y dolor real de despliegue acoplado → extracción quirúrgica. |
| ADR-02 | Tenant = tenant_id BIGINT interno (PK/FK, sharding) + UUID público para identificadores expuestos en API. Nombre canónico: tenant_id (no company_id). | company_id; UUID como PK interna (índices más pesados, sharding más caro). | Ninguno previsto; es decisión de día 1. (Cierra C2 y C3 del debate.) |
| ADR-03 | Shared Schema + Row-Level Security (RLS) como tenancy primario; Silo on-demand (connects_to Rails) para enterprise. | Schema-per-tenant (techo ~5.000 schemas, migraciones O(N)); DB-per-tenant para todos (costo). | Tenant enterprise exige residencia/aislamiento físico → graduación a Silo. |
| ADR-04 | Un solo bus de integración: Outbox transaccional → relay Sidekiq → Amazon EventBridge. CDC/Kafka diferido a Fase 3 (solo si BI exige latencia <5 min). | Tres sistemas en paralelo (Redis/Sidekiq + SNS/EventBridge + Kafka/MSK). | Necesidad medida de streaming BI sub-5-min a escala → introducir CDC, no antes. (Cierra C4, C6, RT3, RT4.) |
| ADR-05 | API-first híbrida: REST /api/v1 (JSON:API-like) como contrato canónico para SUNAT/bancos/UI; GraphQL como gateway de lectura agregada (BFF) para dashboards/BI a partir de Fase 2; webhooks firmados salientes; API Gateway + marketplace de integraciones. | GraphQL como entrada única (N+1, autorización por campo, complejidad fiscal). | — |
| ADR-06 | CQRS ligero: comandos vía service objects/interactors; lecturas pesadas vía vistas materializadas + read replicas. | Event Sourcing completo (sobre-ingeniería v1). | Módulo de auditoría forense que justifique event sourcing acotado. |
| ADR-07 | Pooler + RLS validado con SET LOCAL dentro de transacción explícita, probado a través de RDS Proxy en CI. | SET de sesión sin transacción (riesgo de fuga cross-tenant vía multiplexado del pooler). | — (es bloqueante de seguridad; ver §4.4 y §9-RT1.) |
Decisión de gobierno: este documento es el ADR-MASTER. Cualquier desviación requiere un ADR nuevo aprobado por el Solution Architect + CTO, no una decisión local de un equipo.
2. Vista C4 — Contexto (Nivel 1)
3. Mapa de Bounded Contexts (DDD)
El producto se descompone en bounded contexts alineados a los módulos del negocio. Cada context tiene su propio lenguaje ubicuo, sus agregados, sus eventos de dominio y una Published API explícita (Sales::PublicApi). La comunicación por defecto es asíncrona por eventos; las invariantes que exigen consistencia ACID se resuelven in-process dentro de una sola transacción (marcadas como síncronas).
3.1 Diagrama de contexts y flujo de eventos
3.2 Catálogo de bounded contexts
| Context | Lenguaje ubicuo (términos) | Agregados raíz | Eventos publicados clave | Relación de dominio |
|---|---|---|---|---|
| IdentityAccess | Usuario, Rol, Permiso, Membership, Sesión | User, Role, Membership | UserInvited, RoleAssigned | Upstream conformista de todos (síncrono). |
| Organization | Empresa (RUC), Sucursal, Almacén, Parámetro | Company, Branch, Warehouse | BranchCreated, SettingsChanged | Shared kernel operativo (síncrono). |
| Inventory/Kardex | Producto, StockItem, Lote, Capa de costo, Promedio móvil | KardexLedger, Product | StockValued, StockReserved, StockAdjusted | Customer/Supplier de Sales, Purchasing, Manufacturing, Imports. |
| Purchasing | OC, Recepción, Factura proveedor, Detracción | PurchaseOrder, GoodsReceipt | GoodsReceived, InvoiceBooked | Supplier de Inventory y Accounting. |
| Sales | Cotización, Nota de venta, CPE, NC/ND | SalesOrder, Invoice | SaleConfirmed, CreditNoteIssued | Supplier de Inventory, Accounting, Treasury, CRM. |
| Manufacturing | BOM multinivel, OP, WIP, merma, CIF | WorkOrder, BillOfMaterials | MaterialConsumed, FGProduced, WIPPosted | Customer de Inventory; Supplier de Accounting. |
| Logistics | Despacho, Guía de Remisión (GRE), MTC | Dispatch, RemissionGuide | Dispatched, GRESent | Supplier de Sales. |
| Imports | DUA/DAM, FOB, CIF, landed cost, agente de aduana | ImportFile | LandedCostAllocated, ImportNationalized | Supplier de Inventory y Accounting. |
| Accounting | PCGE, Asiento, Partida doble, Plantilla, Periodo | JournalEntry, Account, PostingTemplate | EntryPosted, PeriodClosed | Customer (vía subscribers) de casi todos; Supplier de Treasury y FinancialStatements. |
| Treasury | Banco, CxC, CxP, Detracción, Conciliación | BankAccount, Receivable, Payable | PaymentSettled, Reconciled | Customer de Accounting y Sales. |
| FinancialStatements | EEFF, PLE, SIRE, RVIE, RCE | StatementRun, SireSubmission | SireGenerated, PleExported | Customer de Accounting y Treasury. |
| HumanResources | Legajo, Contrato, Trabajador | Employee | EmployeeHired | Supplier de Payroll. |
| Payroll | Planilla, AFP/ONP, PLAME, CTS | PayrollRun | PayrollPosted | Supplier de Accounting. |
| CRM | Cliente, Pipeline, Actividad | Customer, Opportunity | OpportunityWon | Customer de Sales. |
| BusinessIntelligence | Dashboard, KPI, Métrica, Cube model | Dashboard (read models) | — (consumidor) | Downstream consumidor de eventos. |
| AI | Copiloto, Forecast, Anomalía, Token meter | ForecastModel, Conversation | AnomalyDetected | Downstream; habla con Cube (capa semántica), no SQL ad-hoc. |
3.3 Reglas de dependencia (enforced por packwerk en CI)
SharedKernelno depende de nadie; todos dependen de él.- Operaciones y Finanzas nunca se referencian por clase directamente — solo por eventos o por
PublicApiexplícita. IdentityAccessyOrganizationson las únicas dependencias síncronas permitidas en cualquier request (tenant y usuario están en todo flujo).- Un context fail-closed: una operación sin
tenant_idresuelto aborta (no asume default). - El context map sigue patrones DDD de relación: la mayoría son Customer/Supplier vía eventos; las integraciones externas (SUNAT, bancos) usan Anti-Corruption Layer (ACL) dentro del pack que las consume, de modo que ningún modelo de dominio conoce el formato UBL/SUNAT.
4. Arquitectura interna por capas (patrones tácticos)
4.1 Anatomía de un bounded context
| Patrón | Aplicación en ConTodo | Justificación |
|---|---|---|
| Aggregate / Aggregate Root | KardexLedger, JournalEntry, WorkOrder encapsulan invariantes (partida doble cuadrada, stock no negativo, BOM válido). | Las invariantes contables/inventario no pueden vivir dispersas en controllers. |
| Command / Interactor | Un caso de uso = un service object (Sales::ConfirmSale, Accounting::PostJournalEntry). | Testeable unitariamente, idempotente, transaccional. |
| Domain Event | Cambio de estado relevante para otros contexts. | Desacopla operación de contabilidad/BI. |
| Outbox | El evento se persiste en la misma transacción que el cambio de estado. | Evita dual-write inconsistente (DB commit + broker). |
| Subscriber / Policy de posteo | Accounting::OnSaleConfirmed mapea evento → asiento vía PostingTemplate. | Las plantillas contables son editables por el contador, versionadas, con dry-run y auditoría (foso vs CONCAR/SISCONT). |
| Anti-Corruption Layer | Adapters SUNAT/OSE/bancos traducen formatos externos al lenguaje ubicuo. | Aísla el dominio de cambios regulatorios externos. |
| CQRS ligero | Escritura por comandos; lectura de EEFF/kardex valorizado por vistas materializadas en read replica. | Reportes no compiten por locks con la operación transaccional. |
| Idempotency Key | Header Idempotency-Key en POST financieros; event_id como clave en subscribers. | Evita asientos duplicados ante reintentos at-least-once. |
4.2 Multi-tenancy — defensa en profundidad
| Capa | Mecanismo | Falla segura |
|---|---|---|
| HTTP | Middleware resuelve tenant; rechaza request sin tenant. | 401/403 |
| Autorización | Pundit policy por módulo × sucursal × acción (RBAC + ABAC + SoD). | 403 |
| ORM | Tenantable default scope + asignación automática de tenant_id. | Excepción si falta Current.tenant_id |
| Transacción | SET LOCAL app.current_tenant dentro de TX (no SET de sesión). | — |
| Motor (RLS) | Policy USING (tenant_id = current_setting('app.current_tenant')::bigint). | 0 filas (no fuga) |
| Background | TenantSetter.with(args[:tenant_id]) envuelve cada job Sidekiq. | Job aborta sin tenant |
4.3 Particionado y escala de datos
- Tablas calientes (
comprobantes,kardex_movimientos,asientos_contables,outbox_events) particionadas portenant_id(HASH) y/o fecha (RANGE) desde el MVP — contiene noisy-neighbor y acelera VACUUM. - Reconocido el riesgo de desbalance por power-law (el 5% de tenants genera ~50% de carga): el particionado por
tenant_idno resuelve al tenant gigante dentro de su partición; la mitigación real es promoción a Silo con disparador explícito (CPU sostenido > 70% en RDS escalada al máximo, o > X% de IOPS atribuible a un solo tenant). Ese ADR de sharding/promoción debe existir antes de 500 tenants, con dueño (RACI). - Lectura BI/EEFF sobre read replicas vía
connected_to(role: :reading).
4.4 [BLOQUEANTE] Pooler + RLS — el riesgo de seguridad #1
El debate técnico identificó como riesgo crítico (RT1) que RDS Proxy/PgBouncer en modo transaction multiplexa conexiones y puede filtrar el contexto de tenant entre requests si se usa SET de sesión. Mandato vinculante (ADR-07):
- Toda query debe correr dentro de transacción explícita con
SET LOCAL app.current_tenant(scope transaccional, no de sesión). - El pooler opera en modo
transactiony nunca se confía una variable de sesión a través de checkouts. - CI ejecuta un test de aislamiento cross-tenant que corre a través del pooler, no solo contra Postgres directo.
- Defensa adicional: validar/re-aplicar
app.current_tenanttras cada checkout de conexión.
Sin esta validación, la promesa de aislamiento es papel y un incidente sería un evento reportable bajo Ley 29733.
5. Arquitectura Event-Driven (eventos + outbox)
5.1 Decisión consolidada: un solo bus
El debate detectó hasta tres sistemas de mensajería propuestos en paralelo (Sidekiq/Redis, SNS/EventBridge, Kafka/MSK). Se colapsan a uno (ADR-04):
Frontera outbox vs CDC (cierra RT3): en MVP y escala media, el outbox es la única fuente de eventos, y alimenta tanto integración (EventBridge → webhooks) como BI (batch nocturno dbt + DuckDB/S3). CDC/Debezium/Kafka no se introduce salvo necesidad medida de latencia BI sub-5-min a escala, lo que ahorra US$ 1.500–2.500/mes y enorme complejidad operativa temprana para un equipo de 4–6.
5.2 Flujo transaccional del outbox
5.3 Contrato del evento y garantías
CREATE TABLE outbox_events (
id BIGSERIAL,
tenant_id BIGINT NOT NULL, -- canonico (ADR-02), particion HASH
event_id UUID NOT NULL UNIQUE, -- idempotency key
name TEXT NOT NULL, -- 'sales.sale_confirmed'
schema_ver INT NOT NULL DEFAULT 1,
aggregate TEXT NOT NULL, -- 'Sale#1234'
payload JSONB NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ,
PRIMARY KEY (id, tenant_id)
) PARTITION BY HASH (tenant_id);
CREATE INDEX idx_outbox_unpublished ON outbox_events (id) WHERE published_at IS NULL;
| Atributo | Garantía |
|---|---|
| Entrega | At-least-once → consumidores idempotentes por event_id con constraint único. |
| Orden | Por agregado (no global); relay FIFO por id con SKIP LOCKED. |
| Versionado | schema_ver permite evolución de payload sin romper consumidores antiguos. |
| Reproceso | outbox:replay re-publica rango por fecha para reconstruir read models BI. |
| Lag | Métrica + alerta si now() - occurred_at > 60s; autoscaling de workers critical. |
5.4 Colas Sidekiq (con política Spot)
| Cola | Prioridad | Contenido | Compute | SLA |
|---|---|---|---|---|
critical | 10 | Outbox relay, asientos, idempotencia, emisión CPE/GRE. | On-Demand (nunca Spot) | < 5 s |
default | 5 | Webhooks, notificaciones, recálculo kardex. | Mixto | < 30 s |
reports | 2 | EEFF, exportes PLE/SIRE, refresh vistas materializadas. | Spot | minutos |
ai | 1 | Forecast, detección de anomalías, OCR. | Spot | batch |
Política (cierra RT6/T4): la cola
criticalcorre On-Demand explícito en Terraform — un Spot interrumpido a mitad de emisión de un CPE o un asiento es un riesgo fiscal inaceptable. Soloreportsyaiusan Spot.
6. Arquitectura API-First
El producto se diseña API-first: la API es el producto, la UI es el primer cliente. Esto habilita el marketplace de integraciones y el modelo de partners.
6.1 Capas de la plataforma de APIs
6.2 REST — contrato canónico
| Aspecto | Decisión |
|---|---|
| Versionado | URL path /api/v1. Deprecación con header Sunset + 6 meses de solape. |
| Formato | JSON:API-like; errores RFC 7807 (application/problem+json). |
| Paginación | Cursor (?cursor=...&limit=50) en listados grandes; offset en catálogos pequeños. |
| Idempotencia | Idempotency-Key obligatorio en POST de mutación financiera (almacenado 24h en Redis). |
| Filtros | ?filter[status]=posted con whitelist por endpoint (Ransack acotado). |
| Rate limit | rack-attack por tenant_id + IP; cuotas por plan SaaS, reforzadas en API Gateway. |
| Auth | Authorization: Bearer <JWT> (access 15 min) + refresh rotativo; OAuth2 scopes para integraciones de terceros. |
| Fuente de verdad | OpenAPI publicado por Rails (cierra el gap de ownership del spec): los tipos del frontend TS se generan desde OpenAPI; breaking changes pasan por proceso de versionado con dueño asignado. |
6.3 GraphQL — gateway de lectura (BFF)
GraphQL no es la entrada única (evita N+1 y autorización por campo en flujos fiscales). Se introduce desde Fase 2 como gateway de lectura agregada para dashboards y BI, donde un cliente necesita componer datos de varios contexts en una sola llamada. La autorización reusa Pundit; la resolución de KPIs pasa por la capa semántica Cube (una sola verdad de KPI compartida con IA NL2SQL, evitando lógica duplicada).
6.4 Webhooks salientes
Emitidos desde el outbox/EventBridge, firmados HMAC-SHA256, con reintento exponencial y Dead Letter Queue. Cada webhook tiene estado por documento y panel de reentrega para el tenant.
| Integración | Tipo | Patrón |
|---|---|---|
| Facturación electrónica (OSE) | Saliente | Webhook + cola dedicada con reintentos; estado por documento. |
| SIRE / PLE SUNAT | Saliente (batch) | Job genera TXT/JSON desde vistas; ACL traduce a formato SUNAT. |
| Tipo de cambio SBS/SUNAT | Entrante | Cron diario → SharedKernel::ExchangeRate. |
| Pagos (Yape/Plin/Niubiz/Izipay) | Bidireccional | Webhook entrante firmado → Treasury::PaymentSettled. |
| E-commerce / Marketplace | Bidireccional | Webhooks + REST sobre el marketplace. |
6.5 API Gateway y Marketplace de integraciones
- API Gateway gestiona API keys, planes/quotas por tier SaaS, throttling y métricas de consumo por partner — separando el control comercial de la lógica de negocio.
- Marketplace de integraciones: catálogo de conectores (e-commerce, logística, pasarelas) construidos por partners sobre la API pública + webhooks + OAuth2 scopes. Es una línea de ingresos y un efecto red (oportunidad O3 del backend): cada integración nueva aumenta el valor de la plataforma.
7. Vista C4 — Componentes (Nivel 3)
Detalle del contenedor API ConTodo (Rails) mostrando los bounded contexts como componentes y sus relaciones de evento.
7.1 Coherencia de dominio reflejada en componentes (debate-03)
Tres incoherencias contables del debate de dominio se reflejan como requisitos de diseño de componentes que esta arquitectura debe soportar:
| Requisito de dominio (P0) | Impacto arquitectónico |
|---|---|
| Asiento de nacionalización desdoblado (proveedor exterior / Aduanas / agente). | Imports publica ImportNationalized con payload de 3 contrapartidas; Accounting::OnImportNationalized genera asiento multi-acreedor con sustento DUA. |
| DUA/DAM (tipo doc 50) como sustento de crédito fiscal en RCE. | El modelo de JournalEntry y el generador SIRE/RCE soportan tipo de documento de sustento aduanero, no solo factura. |
| Plantillas de producción WIP→FG→COGS (circuito clase 6→9→23→21→69). | Manufacturing emite MaterialConsumed/WIPPosted/FGProduced; Accounting tiene PostingTemplate para cada uno → el costo del Kardex tiene espejo en el mayor. |
| IGV 16% + IPM 2% modelado interno, presentado 18%. | SharedKernel parametriza tributos versionados; presentación consolidada es una vista, no una cuenta. |
| Detracciones por código de bien/servicio + umbral S/ 700. | Tabla tabla_detracciones parametrizada y versionada; nunca 12% hardcodeado. |
8. Patrones arquitectónicos transversales (resumen)
| Categoría | Patrón | Dónde |
|---|---|---|
| Estructura | Modular Monolith + Bounded Contexts | packwerk packs |
| Aislamiento | Shared Schema + RLS + Silo on-demand | PostgreSQL / connects_to |
| Mensajería | Outbox transaccional + relay + EventBridge | Operaciones → Finanzas/BI |
| Consistencia | ACID in-process para invariantes; eventual para cross-context | Comandos vs subscribers |
| Lectura/escritura | CQRS ligero (vistas materializadas, read replicas) | BI, EEFF, kardex |
| Fiabilidad | Idempotency keys (HTTP + eventos), DLQ, retry exponencial | POST financieros, webhooks |
| Integración | Anti-Corruption Layer | Adapters SUNAT/OSE/bancos |
| API | API-first (REST canónico + GraphQL BFF + webhooks + Gateway + marketplace) | Edge/API |
| Seguridad | Defensa en profundidad (WAF → Pundit → scope → SET LOCAL → RLS) | Todas las capas |
| Evolución | Extracción quirúrgica a microservicio bajo disparador | Pack con perfil de escala propio |
| Migración | Expand/contract + pg-osc + runbook (no rails db:migrate ingenuo a escala) | DB ops |
9. Riesgos arquitectónicos consolidados
| ID | Riesgo | Severidad | Mitigación |
|---|---|---|---|
| RT1 | Fuga cross-tenant vía connection pooler (RDS Proxy multiplexa SET). | Crítica | SET LOCAL en TX + test de aislamiento a través del pooler en CI (ADR-07, §4.4). Bloqueante de seguridad #1. |
| R2 | Eventos duplicados → asientos dobles. | Alta | Consumidores idempotentes con idempotency_key = event_id + constraint único. |
| RT3 | Doble pipeline de eventos (outbox + CDC) duplica carga WAL. | Alta | Outbox como única fuente; CDC diferido a Fase 3 (ADR-04). |
| RT4 | Tres sistemas de mensajería. | Alta | Colapsado a Sidekiq (jobs) + EventBridge (integración); MSK solo si se justifica. |
| RT5 | Migración a escala bloquea a todos los tenants simultáneamente. | Alta | Expand/contract + pg-osc + runbook desde la primera tabla. |
| RT6 | Spot interrumpe jobs fiscales. | Media-Alta | Cola critical On-Demand explícito en Terraform. |
| RT7 | Sharding/promoción a Silo sin disparador ni dueño. | Alta | ADR de sharding con métrica gatillo + RACI antes de 500 tenants. |
| R3 | Modular monolith deriva en "big ball of mud". | Media | packwerk en CI bloquea dependencias no declaradas; PublicApi explícitas. |
| R4 | Outbox relay lag (reportes obsoletos). | Media | Métrica de lag + autoscaling de critical + alerta > 60s. |
| RT10 | tenant_id UUID vs BIGINT inconsistente rompe RLS en runtime. | Media | BIGINT interno + UUID público fijado día 1 (ADR-02). |
| R5 | Cadena costo importación→producción→venta no cierra en el mayor. | Alta (dominio) | Plantillas WIP/FG/COGS + asiento de nacionalización desdoblado + DUA en RCE (§7.1). |
10. Oportunidades arquitectónicas
| # | Oportunidad | Valor |
|---|---|---|
| O1 | Outbox como única fuente de eventos elimina MSK/Debezium en MVP. | Ahorra US$ 1.500–2.500/mes y complejidad operativa temprana. |
| O2 | API pública + webhooks + marketplace habilita partners. | Nueva línea de ingresos y efecto red. |
| O3 | Tier "Soberanía" (Silo + BYOK + Llama-VPC) como SKU enterprise. | Diferenciador vs Defontana/StarSoft; mayor ticket. |
| O4 | Capa semántica Cube como contrato único de KPIs sirve a BI y a IA (NL2SQL). | Menos alucinación, una sola verdad de negocio. |
| O5 | Tests de aislamiento cross-tenant en CI como evidencia auditable. | Acelera SOC 2 / ISO 27001; activo de venta. |
| O6 | Extracción quirúrgica de packs a microservicio bajo demanda real. | Escala selectiva sin reescritura. |
| O7 | Importador desde CONCAR/SISCONT vía ACL. | Reduce el costo de cambio — principal obstáculo de adopción real. |
11. Conclusión
La arquitectura de software de ConTodo combina modular monolith + DDD por bounded contexts + outbox transaccional + RLS multi-tenant + API-first, una composición deliberadamente probada y operable por un equipo pequeño, sin renunciar a la integridad contable ACID que un ERP fiscal exige. El valor de este entregable no está en las piezas —cada especialista ya las diseñó con criterio senior— sino en el ensamble reconciliado: fija un único tenant_id (BIGINT interno + UUID público), un único bus de eventos (outbox → EventBridge, con CDC diferido), una única estrategia de API (REST canónico + GraphQL BFF + webhooks + Gateway + marketplace) y, sobre todo, blinda el riesgo de aislamiento cross-tenant en el pooler que, de materializarse, sería un evento de extinción para un ERP contable. Las contradicciones inter-documento del debate técnico quedan cerradas como ADRs vinculantes, y las cinco incoherencias contables del debate de dominio se traducen en requisitos de diseño de componentes verificables. Con este contrato, la arquitectura pasa de "siete documentos excelentes que no negociaron entre sí" a un único plano ejecutable, listo para escribir la primera línea de código sin deuda de gobierno.