Trabajando con eventos
En sistemas modernos, siguiendo buenas prácticas de arquitectura como DDD (Domain-Driven Design) y Clean Architecture, los eventos son mecanismos fundamentales para modelar cambios de estado y la comunicación asíncrona entre componentes o sistemas. Un evento representa algo que ya ha ocurrido en el dominio de la aplicación y que puede ser de interés para otras partes del sistema o servicios externos. Eventos en Lino también cubren shadow entities y el flujo Transactional Outbox, integrando DDD, MediatR, MassTransit, RabbitMQ, Hangfire y Unit of Work para publicar mensajes confiables.
Eventos de Dominio
Los eventos de dominio representan hechos importantes que ocurrieron dentro del contexto de la aplicación. Se generan y consumen internamente en el sistema, permitiendo que los cambios de estado se propaguen de manera desacoplada, es decir, sin que los objetos necesiten conocerse directamente entre sí.
¿Cuándo utilizar un Evento de Dominio?
- Siempre que una operación genere un cambio significativo en el dominio y sea necesario notificar o activar otras partes del sistema.
- Para mantener el modelo de dominio cohesionado, permitiendo que diferentes procesos se activen sin crear dependencias directas entre módulos o servicios.
En Lino, crear un nuevo evento de dominio es sencillo. Puede ejecutar:
lino event new lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event edit --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>
El asistente de CLI solicitará:
- Servicio – Servicio en el que se creará el evento.
- Módulo – Módulo en el que se creará el evento (solo en servicios modulares).
- Entidad – Entidad en la que se creará/asociará el evento.
- Tipo de evento – Evento de dominio o evento de integración.
- Nombre del evento – Nombre utilizado en el dominio, asociado a la entidad.
Ejemplo
Al crear el evento de dominio UserCreated asociado a la entidad User,
el sistema crea automáticamente un evento con el nombre UserCreatedDomainEvent.
Este nombre deja claro a cualquier parte del sistema que consuma el evento que la acción de creación de usuario ya se completó.
Estructura generada por Lino:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── Users/
├── User.cs
├── Errors/
├── Events/
│ └── UserCreatedDomainEvent.cs
├── Repositories/
└── Resources/
Flujo técnico en Lino
- El aggregate registra un
IDomainEventmedianteRegisterDomainEvent. - El command handler guarda con
IUnitOfWork, preferiblementeSaveChangesInTransactionAsynccuando hay eventos. - La Unit of Work persiste cambios y publica eventos por MediatR.
- Un domain handler puede registrar un integration event en
IOutbox.
No use eventos como comandos disfrazados; nombre eventos en pasado, como UserCreatedDomainEvent.
Manejadores de Eventos de Dominio
Un Domain Event Handler es una clase responsable de reaccionar a un Evento de Dominio, ejecutando acciones relacionadas con el estado interno de la aplicación, siempre dentro del mismo contexto transaccional.
El objetivo principal de estos manejadores es mantener el sistema cohesionado y desacoplado, permitiendo que se apliquen reglas adicionales sin sobrecargar la lógica central de la entidad o del agregado.
Por ejemplo, después de crear un User, puede ser necesario actualizar estadísticas, generar un registro interno o notificar a otro agregado.
Estas acciones tienen sentido dentro del dominio y pueden ocurrir de manera síncrona, asegurando consistencia inmediata.
Sin embargo, las operaciones que dependen de recursos externos —como el envío de correos electrónicos o llamadas a APIs de terceros— no deben ejecutarse directamente a partir de eventos de dominio, ya que esto ataría la transacción a tareas lentas o inestables. En estos casos, el dominio puede generar un evento de integración (registrado en Outbox), que será procesado posteriormente de manera asíncrona y resiliente.
Características principales:
- Reacciona a un evento, pero nunca lo modifica.
- Ejecuta únicamente operaciones in-process relacionadas con la consistencia del dominio.
- Garantiza que todo se ejecute dentro de la misma transacción.
Para crear un nuevo manejador de eventos de dominio, simplemente ejecute el comando:
lino event-handler new
El asistente de CLI solicitará:
- Servicio – Servicio en el que se creará el manejador de eventos.
- Módulo – Módulo en el que se creará el manejador de eventos (solo en servicios modulares).
- Entidad – Entidad en la que se creará el manejador de eventos.
- Tipo de evento – Evento de dominio o evento de integración.
- Evento – Evento que será consumido.
- Nombre del manejador de eventos – Nombre utilizado que se asociará a la entidad y al evento de dominio.
Ejemplo
Al crear el manejador de eventos de dominio UserCreated asociado a la entidad User y al evento UserCreatedDomainEvent, el sistema crea automáticamente un manejador de eventos con el nombre UserCreatedDomainEventHandler.
Estructura generada por Lino:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── Users/
├── Commands/
├── EventHandlers/
│ └── Domain/
│ └── UserCreatedDomainEventHandler.cs
├── Logging/
├── Queries/
└── Resources/
Responsabilidades técnicas
- Implementa
IDomainEventHandlery reacciona dentro del límite de la aplicación. - Ejecuta consistencia interna, logging, tracing o registro de integración.
- Usa
IOutbox.RegisterIntegrationEventpara crear mensajes confiables. - Mantiene cambio de negocio y
OutboxMessageen la misma transacción.
Eventos de Integración
Los Eventos de Integración son mensajes que indican que algo importante ha ocurrido y deben compartirse con sistemas externos u otros microservicios.
A diferencia de los eventos de dominio, aquí el objetivo es la comunicación entre sistemas y la sincronización de estados.
Cuándo crear un Evento de Integración:
- Cuando un cambio en tu sistema necesita reflejarse en otro sistema.
- Cuando tu microservicio necesita publicar cambios para que otros microservicios puedan reaccionar.
Principales diferencias entre Domain Events y Integration Events:
| Aspecto | Evento de Dominio | Evento de Integración |
|---|---|---|
| Público objetivo | Interno | Externo |
| Acoplamiento | Bajo (interno) | Necesario (entre sistemas) |
| Tiempo de procesamiento | Inmediato | Puede ser asíncrono, con garantías de entrega |
| Persistencia requerida | No obligatoria | Sí (para confiabilidad y resiliencia) |
En Lino, crear un nuevo evento de integración es sencillo. Puedes ejecutar:
lino event new lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>
El asistente del CLI solicitará:
- Servicio – Servicio en el cual se creará el evento.
- Módulo – Módulo en el que se creará el evento (solo en servicios modulares).
- Entidad – Entidad en la que se creará / asociará el evento.
- Tipo de evento – Evento de dominio o evento de integración.
- Nombre del evento – Nombre utilizado para el evento de integración, asociado a la entidad.
Ejemplo
Creando el evento de integración UserCreated asociado a la entidad User, el sistema crea automáticamente un evento con el nombre UserCreatedIntegrationEvent.
Este nombre deja claro para cualquier parte del sistema que consuma el evento que la acción de creación del usuario ya se ha completado.
Estructura generada por Lino:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Integration.Events/
├── <ProjectName>.<ServiceName>.Integration.Events.csproj
└── Users/
└── UserCreatedIntegrationEvent.cs
Publicación confiable
Un integration event implementa IIntegrationEvent y cruza límites entre módulos, servicios o sistemas. La aplicación registra el evento en IOutbox, la Unit of Work persiste un OutboxMessage y un worker Hangfire publica después por MassTransit/RabbitMQ.
Manejadores de Eventos de Integración
Un Integration Event Handler es una clase responsable de consumir un Evento de Integración, normalmente publicado por otro servicio o contexto, y luego ejecutar acciones específicas en su propio dominio.
Estos manejadores reciben eventos a través de mecanismos de mensajería (como RabbitMQ, Kafka, Azure Service Bus, etc.), generalmente junto con el patrón Outbox, que garantiza una entrega confiable y procesamiento asíncrono.
Por ejemplo, cuando un evento UserCreated es publicado por un servicio de Identidad, el manejador correspondiente en otro contexto puede reaccionar enviando un correo electrónico de bienvenida o llamando a APIs externas.
Estas operaciones pueden ser más lentas o propensas a fallos, pero al manejarse fuera de la transacción principal, no comprometen la consistencia interna de la aplicación.
Características principales:
- Reacciona a eventos que representan hechos de negocio relevantes para otros contextos.
- Ejecuta operaciones que pueden ser lentas o externas (por ejemplo: envío de correos electrónicos, llamadas a APIs).
- Se procesa de manera asíncrona y resiliente, a menudo con reintentos y monitoreo.
- Asegura que fallos externos no afecten la transacción original del dominio.
- Facilita la integración entre bounded contexts y sistemas distribuidos.
Para crear un nuevo manejador de eventos de integración, simplemente ejecute el comando:
lino event-handler new lino event-handler new --name <EventHandlerName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event-handler list --service <ServiceName> --module <ModuleName> --entity <EntityName>
El asistente de CLI solicitará:
- Servicio – Servicio en el cual se creará el manejador de eventos.
- Módulo – Módulo en el que se creará el manejador de eventos (solo en servicios modulares).
- Entidad – Entidad en la que se creará el manejador de eventos.
- Tipo de evento – Evento de dominio o evento de integración.
- Servicio – Servicio en el que existe el evento que se consumirá.
- Módulo – Módulo en el que existe el evento que se consumirá (solo en servicios modulares).
- Entidad – Entidad en la que existe el evento que se consumirá.
- Evento – Evento que se consumirá.
- Nombre del manejador de eventos – Nombre que se asociará a la entidad y al evento de integración.
Ejemplo
Creando el manejador de eventos de integración SendEmailOnUserCreated asociado a la entidad User y al evento UserCreatedIntegrationEvent,
el sistema crea automáticamente un manejador de eventos con el nombre SendEmailOnUserCreatedIntegrationEventHandler.
Estructura generada por Lino:
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── Users/
├── Commands/
├── EventHandlers/
│ └── Integration/
│ └── SendEmailOnUserCreatedIntegrationEventHandler.cs
├── Logging/
├── Queries/
└── Resources/
Guía operacional
- Implementa
IIntegrationEventHandlery consume mensajes con MassTransit. - No lee directamente la base del productor.
- Actualiza estado local, proyecciones o shadow entities.
- Debe ser idempotente porque retries y mensajes duplicados pueden ocurrir.
Shadow Entities
Una shadow entity es una copia local, mínima y controlada de datos cuyo dueño está en otro módulo o servicio. Existe para reducir el acoplamiento cuando el consumidor necesita consultar, validar o mostrar datos de referencia sin acceder directamente a la base de datos del productor.
Las Shadow Entities son útiles cuando la consistencia eventual es aceptable. El productor publica un evento de integración, el mensaje se persiste en el Outbox, el worker lo publica en el bus y el consumidor actualiza su copia local mediante un integration event handler.
lino shadow-entity new lino event new lino event-handler new
El comando también puede accederse mediante el alias lino shadow new. En el flujo interactivo, Lino pregunta el servicio o módulo de destino, la entidad de origen y qué propiedades deben copiarse. Esto evita recrear manualmente el tipo de identificador, obligatoriedad, longitud de string y referencias básicas, reduciendo inconsistencias entre origen y consumidor.
La shadow entity no es una réplica completa. En el ejemplo de un módulo Catalog que consume datos de Tenancy y Security, el catálogo puede mantener solo Tenant.Id, Tenant.Slug, User.Id, User.Email y User.TenantId. La entidad original de usuario sigue conteniendo hash, confirmación, tokens, fechas y otros campos internos que no pertenecen al contexto del catálogo.
Cuándo usar
- Cuando el consumidor necesita pocos campos de una entidad que pertenece a otro contexto.
- Cuando la regla acepta un pequeño retraso entre el cambio en el productor y la actualización en el consumidor.
- Cuando leer directamente la base de datos del productor crearía acoplamiento indebido entre módulos o servicios.
- Cuando grids, filtros, validaciones locales o proyecciones necesitan datos de referencia externos.
Cuándo evitar
- Cuando la regla exige datos actualizados al instante y no acepta consistencia eventual.
- Cuando la copia local se convertiría en una réplica completa del aggregate original.
- Cuando el consumidor empieza a depender de invariantes que pertenecen al productor.
Flujo recomendado
- Modela la shadow entity solo con los campos necesarios para el consumidor.
- Crea o selecciona el integration event publicado por el productor.
- Crea un integration event handler en el consumidor.
- Implementa un upsert idempotente usando la clave del productor o un identificador natural.
- Registra logs suficientes para diagnosticar retries, mensajes duplicados y fallas permanentes.
Sincronización Orientada por Eventos
El flujo completo combina dominio, aplicación, Unit of Work, Outbox, worker, bus y consumidor. El objetivo es evitar llamadas directas frágiles entre módulos o servicios y permitir retry, auditoría y reprocesamiento cuando la publicación o el consumo falla.
- Un aggregate cambia y registra un evento de dominio.
- La Unit of Work guarda el cambio dentro de una transacción.
- Un domain handler reacciona al hecho y registra un evento de integración en
IOutbox. - La Unit of Work persiste los datos de negocio y
OutboxMessageen la misma transacción. - Un worker Hangfire lee el Outbox y publica en el bus mediante MassTransit/RabbitMQ.
- El consumidor recibe el mensaje mediante un
IIntegrationEventHandler. - El consumidor actualiza su modelo local, shadow entity, proyección, e-mail, integración externa u otro flujo asíncrono.
Domain change -> Domain Event -> Domain Handler -> Integration Event -> Outbox -> Worker -> Event Bus -> Consumer -> Local model
Reglas de diseño
- Coloca las invariantes en el dominio; usa eventos para reacciones, no para ocultar reglas obligatorias.
- Registra integration events dentro de la misma transacción del cambio de negocio.
- Mantén los contratos de integración pequeños, versionables y estables.
- Implementa consumers idempotentes, porque pueden ocurrir retries y mensajes duplicados.
- Monitorea fallas del worker y mensajes detenidos en el Outbox.
Resultado práctico: el productor no necesita conocer a los consumidores, los consumidores no acceden a la base de datos del productor y la comunicación entre contextos sigue siendo resiliente incluso cuando el broker o el consumidor está temporalmente indisponible.
