Files
task-stream/docs/08-decisiones.md
Josep Piguillem efa06a0723 feat: notifications in the app and, optionally, by e-mail
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>
2026-10-08 09:19:59 +02:00

87 lines
4.7 KiB
Markdown

# 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.