ADR-025 · Service topology standard (4 patterns canónicos A/B/C/D)¶
Fecha: 2026-04-29 Source:
/srv/projects/cis/cis-plan/DECISIONS.md(do not edit here — re-split desde la fuente)
Contexto
El audit acdf956053dd737dd (agent-c, 2026-04-29) reportó que la división backend/frontend en CIS no es uniforme: es inconsistencia heredada, no decisión deliberada. Casos representativos:
cis-adminlegacy corre 3 stacks coexistentes: Django + FastAPI :8244 + Next.js :8245. ADR-010 ya decidió colapsar a Vite SPA + FastAPI (B), pero la migración (Fase E) está en curso.cis-maileres FastAPI + UI Jinja2 mínima (A monolítico). Correcto.cis-pagoses FastAPI puro + dos clientes externos consumen su API (A monolítico). Correcto.periodismo2es un cluster de 4 servicios (-www,-api,-admin,-frontend) conPYTHONPATHcruzado. Topología no canónica heredada de migración previa.cis-isometricoes FastAPI + worker-renderseparado (D workers). Correcto.
Sin un canon escrito, cada servicio nuevo elige su topología por inercia o copy-paste del último. Esto produce 2 systemd units cuando 1 alcanza, o PYTHONPATH cruzado cuando un solo backend con 2 routers resuelve.
Alternativas consideradas
- (a) Sin canon — cada servicio elige libremente
- Pro: flexibilidad máxima.
-
Contra: estado actual. Produce inconsistencia. Cuesta razonar sobre el ecosistema. Cutover de un servicio split (cis-admin Fase E) toma coordination de múltiples units.
-
(b) 4 patterns canónicos con migration path para deuda ← propuesta
- Pro: cubre los casos reales (no fuerza 100% monolítico ni 100% split). Migration path documentado para servicios que heredan topología no canónica.
-
Contra: define 4, no 2. Más superficie mental que un canon estricto.
-
(c) Forzar microservicios separados (backend + frontend siempre split)
- Pro: separación clara siempre.
-
Contra: doble systemd unit por servicio. Build pipeline duplicado. Deploy lockstep complejo. Para servicios sin UI rica (cis-mailer, cis-pagos, cis-monitoreo) es overkill.
-
(d) Forzar monolito siempre (FastAPI + Jinja templates o SSR React)
- Pro: simple, una unit.
- Contra: paneles ricos como cis-admin se quedan con UX pobre. Vite+React+TS no cabe naturalmente en monolito sin SSR.
Decisión
4 patterns canónicos. Cada servicio nuevo cae en uno y se documenta en su AGENT-CONTEXT.md:
Pattern A · Monolítico¶
- Forma: FastAPI + UI Jinja2 mínima (templates server-side) · 1 systemd unit.
- Cuándo: servicios sin UI rica (admin con 2-3 pantallas, dashboard simple, API pura con health UI).
- Naming: 1 unit
cis-<svc>. - Ejemplos canon:
cis-mailer(UI dashboard de templates + logs),cis-monitoreo(UI status),cis-pagos,cis-core,cis-sii-gateway,cis-claudia(host MCP + chat backend; UI rica vive encis-admin/chat, no acá).
Pattern B · Backend + SPA estática¶
- Forma: FastAPI API + Vite
dist/servido por Caddyfile_server· 1 systemd unit (la SPA es archivos estáticos, no proceso). - Cuándo: paneles internos con UI rica sin necesidad de SEO/SSR.
- Naming: 1 unit
cis-<svc>-backendo simplementecis-<svc>. - Caddy: bloque dual con
handle /api/*→ reverse_proxy backend, default →root /srv/projects/cis/<svc>/dist; file_server. - Ejemplos canon:
cis-adminSPA nueva (post-Fase E),cis-infra,cis-mando(UI server-side hoy, candidato a B en v0.3). - Pattern preferido para nuevos paneles internos.
Pattern C · Backend + Frontend SSR¶
- Forma: FastAPI API + Next.js SSR/ISR · 2 systemd units (
cis-<svc>-backend,cis-<svc>-frontend). - Cuándo: solo cuando SEO/ISR justifican el costo. Cara al público con artículos indexables, contenido cacheable por edge.
- Ejemplos canon:
periodismo2-frontend(cara al mundo, índice Google). - Antipattern detectado: cis-admin legacy usaba Pattern C sin necesidad SEO (es internal,
noindex). ADR-010 lo migra a B.
Pattern D · Workers / sync separados¶
- Forma: backend + N units adicionales
-sync,-render,-worker· N+1 systemd units. - Cuándo: procesos async legítimos (poll IMAP cada N min, render mesh OSM, queue arq, transcripción Whisper).
- Naming:
cis-<svc>(web),cis-<svc>-sync/cis-<svc>-render/cis-<svc>-worker. - Ejemplos canon:
cis-inbox(webcis-inbox-backend+cis-inbox-syncIMAP poll),cis-isometrico(cis-isometrico-renderpara meshes),cis-usaia(queue Whisper).
Reglas vinculantes:
- 1 systemd unit por componente real (proceso). No multiplicar units sin razón. Un cron interno con
arqoapschedulerno requiere unit separada — vive en el proceso del backend. - Naming:
cis-<svc>para componente único;cis-<svc>-<role>cuando se necesita disambiguar (-backend,-frontend,-sync,-render,-worker). - AGENT-CONTEXT.md declara explícitamente el pattern:
topology: A|B|C|D.
Migration path para deuda:
cis-adminlegacy (Django + FastAPI :8244 + Next.js :8245) → colapsa a B cuando Fase E cierre. ADR-010 + P0.1 ejecuta. Resultado: 1 unitcis-admin-backend+ Caddy sirve SPA estática.periodismo2cluster (4 sub-servicios con PYTHONPATH cruzado) → consolidar a backend único (-api+-adminmerged como 1 unit con 2 routers) + frontend SSR (-frontend) + worker (-render). Resultado: 3 units en lugar de 4. Documentado como tarea T3 en BACKLOG flujo 11. Migration coordinada (Wave 3) por dependencia PYTHONPATH.cis-claudiav1 (subprocess Claude Code monolítico) → Pattern A en v2 (ADR-015). Sin worker separado en MVP; arq queue interna si crece.
Consecuencias
- Positivo: cualquier servicio nuevo cae en un pattern conocido. Cutover y deploy procedure son predecibles. Topología no se elige por inercia. Migration path documentado para los 2 casos de deuda más visibles.
- Negativo: la línea entre A y B no siempre es obvia (cuándo "una pantalla más" justifica saltar a Vite SPA). Heuristica: si la UI tiene >5 vistas con state cliente significativo → B. Si es <5 vistas + form submit + tabla → A.
- Riesgo: PYTHONPATH cruzado en periodismo2 es deuda persistente; consolidación toma 2-3 semanas. Mientras dure, el cluster sigue siendo no-canónico. Documentado como conocido.
- Migración: cis-admin Fase E (P0.1, semana 1) cierra el primer caso. periodismo2 consolidación (Wave 3, semanas 4-6) cierra el segundo. Servicios nuevos arrancan canónicos.
- Compatibilidad: servicios canónicos hoy (cis-mailer, cis-pagos, cis-monitoreo, cis-core, cis-inbox, cis-mando) ya cumplen — no se tocan. La regla aplica a partir de la fecha del ADR.
- Documentación: cada
AGENT-CONTEXT.mdagrega fieldtopology: A|B|C|D+ "components" listando units. Generadorregenerate-ecosystem.sh(ADR-016) lo lee para el mapa global.