Derived from events and kept per person: whoever a ticket is assigned to, whoever reports or works on it when it moves, fails QA or gets a comment, and whoever joins a project. Never about what you did yourself, once per event. They arrive live through a subscription to a bell in the header, and are copied by e-mail to whoever turns it on when SMTP_URL is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.7 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.
Adjuntos en disco, notificaciones fuera del event store
Los adjuntos se registran como eventos del ticket, pero los bytes van a un puerto AttachmentStorage (disco,
ATTACHMENTS_DIR): un event store no es sitio para megabytes. Se escribe el archivo antes que el evento, y si el
evento falla se borra el archivo, así que nunca hay un adjunto apuntando a nada. Coste: el volumen hay que
copiarlo y compartirlo entre réplicas.
Las notificaciones sí salen de eventos, pero no son eventos: si un aviso está leído, o si alguien quiere copia por
correo, solo le importa a esa persona y no necesita historia. Se guardan en MongoDB por destinatario y se marcan como
leídas sin pasar por un agregado. Si se reconstruye el read model, se pueden regenerar desde los eventos (quedarían
como no leídas). Alternativa descartada: un agregado «bandeja» por persona, con eventos NotificationRead; mucha
ceremonia para un estado que nadie audita.