Eventos de dominio y de integración: qué publicar y qué dejar dentro

Una reserva de aula se confirma y nuestro sistema avisa al servicio que abre la puerta a la hora prevista. Al principio parece fácil publicar el mismo evento que usamos dentro del módulo de reservas. El problema aparece cuando cambiamos cómo guardamos las reservas: otro servicio dependía de un campo interno que creíamos privado.

La diferencia entre un evento de dominio y uno de integración no está en que ambos digan «ha pasado algo», sino en quién puede depender de ellos. Dentro de un módulo podemos cambiar un evento junto con su código. Fuera, el mensaje es un contrato con otros consumidores.

Un ejemplo con reservas de aulas

Imaginemos que el módulo de reservas emite ReservaConfirmada para actualizar su calendario. El evento interno puede transportar detalles de la entidad, como un estado numérico o el identificador del usuario que hizo el cambio. Dentro del mismo módulo sabemos lo que significan y podemos refactorizarlos.

El servicio de control de acceso no necesita ese objeto entero. Necesita saber que se ha reservado el aula, cuál es y entre qué horas debe permitir la entrada. Para él publicaríamos un mensaje específico, por ejemplo:

Esquema del evento interno ReservaConfirmada transformado en AulaReservada para control de acceso
El adaptador publica sólo los datos que necesita control de acceso; el evento interno queda dentro del módulo de reservas.
{
  "tipo": "AulaReservada",
  "version": 1,
  "eventId": "evt-73",
  "reservaId": "r-73",
  "aulaId": "a-12",
  "inicio": "2026-09-24T08:00:00Z",
  "fin": "2026-09-24T09:00:00Z"
}

Las horas van con zona horaria explícita y representan la reserva cuando ocurrió el evento. Si el consumidor procesa el mensaje más tarde, no tiene que consultar la reserva actual para averiguar el horario que se confirmó entonces. Tampoco recibe estadoInterno: 4 ni datos personales que no necesita. Es una decisión sobre el contrato, no una copia de nuestra tabla.

Separar el contrato del modelo interno

Una función pequeña puede transformar el evento interno antes de publicarlo. En JavaScript, la parte importante sería seleccionar campos concretos en lugar de hacer { ...reserva } y exponer mañana cualquier campo nuevo por accidente:

function publicarReserva(confirmacion) {
  return {
    tipo: 'AulaReservada',
    version: 1,
    eventId: confirmacion.eventId,
    reservaId: confirmacion.reserva.id,
    aulaId: confirmacion.reserva.aulaId,
    inicio: confirmacion.reserva.inicio,
    fin: confirmacion.reserva.fin,
  };
}

Para no romper el contrato sin darnos cuenta, podemos probarlo con node:test y node:assert/strict, incluidos en Node.js. El test pasa una reserva que también tiene estadoInterno y usuarioId, pero exige que la salida contenga sólo los campos acordados. Junto con la función anterior, lo guardamos en un archivo contrato.test.cjs y lo ejecutamos con node --test contrato.test.cjs:

const test = require('node:test');
const assert = require('node:assert/strict');

test('publica sólo los datos acordados', () => {
  const evento = publicarReserva({
    eventId: 'evt-73',
    reserva: {
      id: 'r-73', aulaId: 'a-12',
      inicio: '2026-09-24T08:00:00Z',
      fin: '2026-09-24T09:00:00Z',
      estadoInterno: 4, usuarioId: 'u-19'
    }
  });
  assert.deepEqual(Object.keys(evento), [
    'tipo', 'version', 'eventId', 'reservaId',
    'aulaId', 'inicio', 'fin'
  ]);
});

Si un consumidor empieza a necesitar otro dato, lo hablamos y lo añadimos de forma compatible cuando sea posible. Si cambiamos el significado de inicio o eliminamos un campo que ya usa, necesitamos una versión nueva y un periodo de convivencia, igual que haríamos con una API. Derek Comartin plantea precisamente que un evento enviado fuera de nuestro límite se convierte en un contrato; no basta con poner un intermediario de mensajes para que los sistemas queden desacoplados.

Publicar después de guardar, y asumir reintentos

Queda un problema práctico: si guardamos la reserva en la base de datos y falla el envío del mensaje, control de acceso nunca se entera. Si enviamos primero y falla el guardado, podría abrir la puerta para una reserva inexistente. El patrón transactional outbox guarda la reserva y el mensaje pendiente en la misma transacción; otro proceso lo entrega después. Aun así, una entrega puede repetirse, por lo que el consumidor debe reconocer eventId y evitar aplicar dos veces la misma reserva.

No todos los proyectos necesitan una cola, un outbox y varios servicios. Si todo vive en la misma aplicación, un evento interno puede ser suficiente. La separación empieza a importar cuando alguien fuera del módulo utiliza el mensaje como promesa estable. Está relacionada con la frontera del dominio que comentamos en este artículo sobre dominio y base de datos.

Antes de publicar el próximo evento, preguntémonos qué necesita de verdad quien lo recibe y qué podremos cambiar mañana sin romperle el trabajo.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *