Files
task-stream/docs/08-decisiones.md
2026-10-03 13:40:26 +02:00

3.8 KiB

8. Decisiones de diseño

Registro breve de las decisiones que más condicionan el código, con su motivo y su coste.

Arquitectura hexagonal con una librería Nx por capa

Las reglas de dependencia las comprueba el lint, no la disciplina del equipo. Coste: más librerías y más ficheros (un command y un handler por fichero, como en el patrón de referencia del equipo).

La misma estructura en API y web

libs/api/<contexto>/<capa> y libs/web/<contexto>/<capa>: quien conoce un lado se orienta en el otro.

Event sourcing en PostgreSQL (y no en MongoDB)

El requisito era escritura en PostgreSQL y lectura en MongoDB. El event store (events) implementa la misma API que el MongoEventStore del patrón de referencia (persist, getEventsByStreamId). Diferencia: un conflicto de versión lanza ConcurrencyError en lugar de solo registrarse, para no perder cambios en silencio.

commitAndWait()

El commit() de @nestjs/cqrs no devuelve la promesa del publisher: el command respondería OK aunque el event store fallara. VersionedAggregateRoot.commitAndWait() espera la escritura y avanza la versión del agregado.

Redis entre escritura y lectura, y un bridge que espera

El requisito era que Redis llevase los datos de escritura a lectura. El EventsBridge hace el papel del change stream del patrón de referencia, pero no usa eventBus.subject$.next(): así los handlers se ejecutarían en paralelo (mergeMap), un IssueStatusChanged podría proyectarse antes que su IssueCreated y un fallo se perdería. El bridge espera a cada @EventsHandler en orden y confirma en Redis (XACK) al final.

EventClsRegistry y @AutowiredEvent en el dominio

En el patrón de referencia están en infraestructura, pero los eventos de dominio los usan; ponerlos allí rompería la regla de capas.

Lookups fuera de los agregados

La unicidad de claves de proyecto, la numeración de tickets y los nombres de release no caben en un agregado. Se resuelven con tablas pequeñas en PostgreSQL. Coste: no forman parte de la misma transacción que los eventos (una clave reservada puede quedar huérfana si el guardado falla después).

Reglas críticas contra el event store, no contra Mongo

"No publicar una release con tickets sin terminar" se comprobaba primero contra el read model y fallaba si alguien metía un ticket y publicaba justo después. Ahora reconstruye esos tickets desde los eventos.

Proxy de tokens en la API

Pireon no permite CORS en /oauth/token y el client_secret no debe llegar al navegador. POST /api/auth/token hace de intermediario, igual que la API de administración de la landing.

Caché de Apollo desactivada en la web

El estado vive en los stores de NgRx (alimentados por queries y subscriptions). Tener dos fuentes de verdad (Apollo cache y stores) solo añadiría inconsistencias.

Estado web con el plugin de eventos de NgRx Signals

Los efectos se declaran con withEventHandlers (nombre en v22 de lo que antes era withEffects), y lo que emiten se despacha automáticamente.

Facades delante de los stores

Las páginas usan una facade por store (BoardFacade, IssueFacade…) y no conocen ni los stores ni los eventos, que ya no se exportan. La facade expone el estado como señales y las intenciones como métodos, y pone el contexto (id del proyecto o del ticket abierto). Así se puede cambiar cómo se gestiona el estado sin tocar las páginas, y la lógica de presentación se prueba sin plantillas. Coste: una clase más por store, casi toda de reenvío.

Imagen de la API con dependencias propias

El package.json raíz incluye Angular y @pi/ui (del Nexus privado). La imagen de la API usa apps/api/package.json, así que ni lleva el frontend ni necesita el token del Nexus en ejecución.