Este post es el primero de una serie dedicada a las arquitecturas orientadas a eventos. En este post vamos a hablar brevemente de las arquitecturas EDA, en qué consisten y qué hay que tener en cuenta a la hora de trabajar en este tipo de arquitecturas.

¿Qué es EDA?

El término EDA es uno de los más populares hoy día si hablamos de software y, en concreto, de arquitectura.

Podemos pensar que lo que define a una arquitectura de este tipo es el intercambio de mensajes entre múltiples servicios, de forma asíncrona, usando un bus de mensajería. Asociado a este pensamiento, también encontramos la creencia de que intercambiar cualquier tipo de mensaje de forma asíncrona favorece un menor acoplamiento y, por ello, se tiende a diseñar los sistemas utilizando solo intercambio asíncrono. Esto lleva a verdaderas obras de ingeniería para implementar comportamientos de negocio que requieren de sincronía mediante soluciones de naturaleza asíncrona, soportadas en herramientas de intercambio de mensajes de este tipo.

EDA significa “Event Driven Architecture”, que traducido literalmente sería “Arquitectura Guiada por Eventos”. Como su propio nombre indica, la clave en este tipo de arquitectura son los eventos (de negocio) como principio de diseño. La clave no es usar microservicios que se comunican de forma asíncrona, sino identificar y definir correctamente los eventos que se producen en el sistema, qué acción los desencadena (y cuándo) y qué se produce a raíz de esos eventos.

Es cierto que, en este tipo de arquitecturas, el intercambio de información suele ser de naturaleza asíncrona, pero esto no quiere decir que no pueda haber intercambio de información de forma síncrona o que haya que forzar a utilizar una herramienta concreta.

Por lo tanto, EDA implica entender muy bien la sucesión y gestión de eventos (de negocio) que se realizan dentro del contexto de un problema o necesidad.

Definiendo “evento”

Como hemos dicho, cuando trabajamos con arquitecturas orientadas a eventos, el elemento principal, valga la redundancia, es el evento. Pero… ¿qué es un evento?

En muchas ocasiones se puede confundir el término y llamar “evento” a un mensaje intercambiado mediante una plataforma o bus de eventos. Cuando hablamos de eventos, la semántica importa, y mucho. Un evento es algo que ha sucedido, un hecho. A veces, los eventos se producen como resultado de acciones que se ejecutan, como resultado de otros eventos o simplemente por cumplirse una condición temporal.

Por tanto, al trabajar en arquitecturas orientadas a Eventos tenemos que ser cuidadosos a la hora de identificar hechos que suceden en el sistema, teniendo claro su origen y si se producen como respuesta a la ejecución de una acción, como resultado de otro evento o como el cumplimiento de una acción temporal. Por lo tanto, tenemos que distinguir entre comandos para ejecutar acciones y eventos. Veamos la diferencia:

Tipos de eventos

Un evento es algo que sucede en el sistema, un hecho consumado, inmutable. Esto no quiere decir que todo lo que suceda en el sistema “importe” a todo el mundo. Habrá eventos que sean únicamente importantes dentro de determinados contextos y habrá otros que interesen fuera de esos contextos. Por ello, a la hora de diseñar sistemas basados en eventos, podemos encontrar diferentes “tipos de eventos” según el escenario y caso de uso:

Me gustaría comentar que no es necesario forzar esta distinción de eventos si vemos que añade complejidad al diseño del sistema. A veces no está muy clara la diferencia entre unos y otros o se pueden usar indistintamente siempre que tengamos cuidado con el acoplamiento funcional.

Acoplamiento en EDA

A la hora de modelar sistemas mediante eventos siempre tenemos que tener el objetivo de modelar dominios y contextos lo más independientes posible. Muchas personas mantienen la teoría de que si estamos en una arquitectura EDA utilizando comunicaciones asíncronas, eliminamos el acoplamiento entre sistemas, ya que productores y consumidores no están directamente enlazados, sino que se comunican mediante un broker de mensajería.

Este principio de que no existe acoplamiento no es del todo correcto. Por ejemplo, imaginemos un sistema dividido en dos dominios: selección y empleados. Aunque ambos podrían caer dentro de un mismo dominio padre, que podría ser “Personas”, Negocio los considera contextos diferentes:

Si analizamos el negocio y contexto de “selección” podemos observar la siguiente secuencia de eventos:

Secuencia de eventos en el negocio y selección

Como podemos observar, eventos como “entrevista realizada” o “precontrato firmado” no tienen importancia o significado dentro del contexto de “empleados”. Además, en ese dominio tampoco necesitan enterarse de ellos. En el dominio de “empleados” sólo se necesita saber que se ha contratado a una persona para preparar el “kit de bienvenida” y asignar un mentor.

Imaginemos que, cuando estamos diseñando el sistema, establecemos que una contratación se ha realizado sí y solo sí cuando se han dado los siguientes eventos:

Se tienen que dar los tres, pero la secuencia temporal no es siempre la misma.

Al hacer esto, lo primero que podemos observar es que se está trasladando lógica de negocio o detalles del contexto de “selección” a “empleados”, ya que “empleados” debe implementar una lógica propia de “selección”. Lo segundo que nos debe llamar la atención es que “empleados” no sabe qué significan realmente esos términos porque no son objeto de su negocio.

Teniendo en cuenta estos dos puntos, ¿qué sucedería si mañana se decide que para dar una contratación por realizada, además de esos tres requisitos o eventos (precontrato firmado, fecha cerrada y documentación validada) se necesita que el nuevo empleado rellene un formulario de incorporación? Tendríamos que modificar la lógica que hemos incluido en el dominio de “empleados” porque con la decisión que hemos tomado anteriormente hemos acoplado ambos dominios desde el punto de vista funcional.

El uso de un broker de mensajería proporciona un bajo acoplamiento técnico, pero no evita los acoplamientos funcionales fuertes, perdiendo la independencia de dominios y contextos. ¿Cómo podríamos evitar ese acoplamiento funcional fuerte? Usando un evento de notificación.

Por ejemplo, podemos plantear el evento “Contratación realizada”:

Evento Contratación realizada

El término “Contratación” sí es algo que interesa a ambos contextos porque marca la relación entre ambos contextos (se puede conocer como “pivote”). Es un concepto que no va a cambiar e identifica de forma inequívoca que una persona pasa de candidato a empleado, independientemente de si “selección” necesita previamente realizar una o quince acciones dentro de su contexto. De este modo, no acoplamos los contextos y pueden evolucionar de forma independiente:

Estructura de un evento

Cuando estamos diseñando una arquitectura orientada a eventos, un punto de discusión habitual es el contenido de un evento porque realmente no hay un estándar oficial dentro de la comunidad. Por ello, la Cloud Native Computing Foundation (CNCF) trabaja en un estándar bajo el proyecto CloudEvent.

Si consultamos la especificación 1.0.2 de CloudEvent core, podemos ver que todo evento debe contener información sobre el hecho (datos) que referencia y sobre el contexto (metadatos) en que se produce el hecho.

A continuación, vamos a detallar qué puede contener cada sección tomando como punto de partida la especificación de CloudEvents. El objetivo de este apartado no es establecer una única estructura de mensaje, sino que pueda servir como punto de partida en cada contexto.

Metadatos

Dentro de los metadatos o información de contexto tenemos que incluir información que nos ayude a conocer el origen del evento, su posible relación con otros eventos o hechos:

Es importante que se defina una estructura clara de metadatos obligatorios que sea la base de la estructura de todos los eventos dentro de una compañía.

Por otro lado, si necesitamos enriquecer estos metadatos y poder dar información adecuada del contexto en cada escenario, se pueden incluir otros datos para dar más información del contexto como, por ejemplo:

Datos

Los datos que contiene el evento dependen del hecho al que referencia, por lo que establecer una estructura común para todos no es viable. Se debe definir un esquema concreto para cada evento.

A la hora de definir ese esquema, hay que tener en cuenta que la información sea la adecuada según el tipo o intención del evento para evitar verbosidad y asegurar la seguridad y privacidad de la información que contienen. Dentro de este punto, se puede optar por cifrar la información que no sea pública o restringir el acceso a cierto tipo de eventos solo a los consumidores que lo necesiten.

Contratos en EDA

Cuando trabajamos en arquitecturas que se basan en el intercambio de mensajes, sea de forma síncrona o asíncrona, disponer de mecanismos formales para establecer cómo va a ser ese intercambio de información es clave. Este mecanismo se conoce como contrato.

Un contrato es un documento que dice cómo vas a intercambiar la información (contrato operacional) y qué información vas a intercambiar (contrato informacional):

Contrato que nos dice cómo intercambiar la información

Cuando trabajamos con APIs tradicionales o síncronas, estamos muy acostumbrados a los contratos y a adoptar un enfoque “API (Contract) First” y sus ventajas (“acelerar” el desarrollo, consenso, autonomía, etc.). En EDA, también podemos y debemos trabajar de esta forma, utilizando contratos. Veamos en qué consiste un contrato en EDA.

Contrato operacional: definiendo tópicos o canales

En una arquitectura EDA, los eventos se distribuyen por medio de unos canales o tópicos a los que se suscriben los consumidores. El acceso a estos tópicos puede tener ciertos requisitos de seguridad. Esta información es la que encontraremos dentro del contrato operacional.

Tópicos o canales

Si trabajas con Kafka seguro que el término “tópico” te resulta bastante familiar. Un tópico es el canal dónde se publican eventos. Es aconsejable que un tópico esté asociado a un único tipo de evento. Es decir, debemos evitar usar “tópicos comodín”, en el que se pueda publicar cualquier evento, siendo un cajón desastre ingobernable.

Además, es importante decidir una estrategia de nombrado desde el primer momento para que el sistema sea usable, consistente y evolucionable. Al igual que con la estructura de los eventos, no hay un estándar y podemos encontrar diversas tendencias. En este post tampoco vamos a fijar un estándar, sino que vamos a enumerar una serie de buenas prácticas en el nombrado:

Por tanto, lo ideal es que el nombre del topic represente su propósito de forma inequívoca. Por ejemplo, podemos adoptar la siguiente convención:

<dominio>.<subdominios>.<tipo>.<clasificación>.<nombre>

Donde:

De esta forma, con el nombre del tópico podemos saber qué tipo de información se publica en él. Por ejemplo, dentro del proceso de contratación, podríamos tener los siguientes topics:

Podemos encontrar convenciones en las que se aconseja añadir un bloque final con la versión del evento asociado para gestionar mejor la evolución de los eventos y los problemas de compatibilidad hacia atrás. También, en el caso de estar en un escenario multi-tenant, en el que el broker de eventos es “compartido” por varias compañías o entornos, puede ser útil añadir un bloque que permita diferenciar de forma sencilla.

Una ventaja de la organización en bloques es que es mucho más fácil la gestión de seguridad o ACLs en caso de ser necesario. Por ejemplo, podemos definir una regla de seguridad que restrinja la escritura de los topics de dominio solo a los componentes del dominio “recruiting” mediante el uso de wildcards: “permitir escritura en recruiting.events.domain.* solo a ciertos usuarios”

Contrato informacional: esquemas

Ya hemos visto cómo distribuir la información, pero todavía no sabemos qué información se va a distribuir. Dentro de esta parte del contrato se especifica el contenido en sí del evento, incluyendo nombre y tipo de los campos, cuáles son obligatorios y cuáles opcionales y también, podemos especificar restricciones de valores para los campos. Todo esto lo definimos mediante esquemas.

Cuando hablamos de eventos y esquemas, los esquemas son la definición inequívoca de la estructura del contenido de esos eventos. Como ya hemos visto anteriormente, el contenido de un evento se define por sus datos y sus metadatos.

Como buena práctica, es aconsejable definir una estructura de metadatos fija para todos los eventos y mensajes, de modo que se facilite su gestión y su explotación. Podríamos decir que los metadatos son las “condiciones generales” del contrato. Los datos, como su estructura es única de cada evento, serían las “condiciones particulares”:

Estructura de datos y metadatos para un contrato y evento

La definición de esquemas permite que tanto productores como consumidores puedan desarrollarse por separado con garantías, de forma autónoma e independiente, sabiendo que van a entenderse porque se eliminan los problemas de integración.

Registro de esquemas

Seguramente hayas oído hablar de los registros de esquemas o incluso ya has trabajado con ellos. Es algo habitual si trabajas con Kafka, por ejemplo.

Cuando un productor diseña el contrato de un evento lo debe publicar en un registro de esquemas para que esté a disposición de toda la organización. El registro de esquemas actúa como la fuente de la verdad de contratos dentro de la organización, soportando también la posterior evolución de los eventos.

Los desarrolladores lo usan para construir tanto productores como consumidores. En tiempo de ejecución, esos productores y consumidores lo usan para construir y validar los mensajes, basándose en los esquemas publicados.

Esquemas que usan los productores y consumidores

Por tanto, el registro de esquemas es una pieza fundamental para garantizar la autonomía e independencia entre los diferentes productores y consumidores permitiendo que el sistema sea gobernable y evolucionable en el futuro. Sin un registro de esquemas, nos encontraremos en poco tiempo con una arquitectura EDA ingobernable.

Evolución y versionado de eventos

Siempre que diseñemos un sistema tenemos que pensar en los requisitos actuales pero teniendo en cuenta la evolución futura del sistema. En el caso de EDA, como en el de cualquier otra arquitectura, es aconsejable que las evoluciones surjan de manera natural, basándose en las necesidades de cada momento.

Aunque parezca obvio, no pensemos en versionar eventos que no estamos usando en producción. Una vez estemos produciendo y consumiendo eventos en producción, sí tenemos que tener cuidado a la hora de evolucionar los eventos.

Una vez más, nos apoyamos en los contratos. Evolucionar un evento implica diseñar un nuevo contrato. Lo normal es que solo modifiquemos el contrato informacional para modificar la información que incluye.

A la hora de modificar este contrato informacional, como buenas prácticas podemos destacar:

Evolución en los eventos de notificación: a tener en cuenta

Una evolución natural en este tipo de arquitecturas es incluir más información en los eventos de notificación o integración a medida que se dispone más información del uso en producción.

Si usas eventos de notificación, debes incluir solo la mínima información que permita que si el consumidor necesita más información pueda realizar una petición (síncrona o asíncrona) al origen.

Ahora bien, podemos caer en la tentación de reducir llamadas incluyendo más información en esos eventos y así reducir latencia y tráfico en la red. A la hora de hacer esto, si “eliminamos” la petición extra para solicitar información a demanda, tenemos que tener en cuenta que:

Por ejemplo, tenemos un evento “contratación realizada” en la que actualmente solo se incluye el NIF. Estudiando producción vemos que un gran porcentaje de llamadas posteriores al evento son para pedir el nombre, apellidos y datos de contacto del contratado. Antes de eliminar esa llamada a demanda, tenemos que evaluar si es rentable y aconsejable incluir esa información en el evento que ya se está consumiendo y que pondremos a disposición de todos los consumidores actuales.

Herramientas

Por último, vamos a hablar de herramientas. Cuando trabajamos con arquitecturas más tradicionales o con mensajería síncrona, las herramientas de las que disponemos son más abundantes y maduras. En arquitecturas EDA todavía no hay tanta madurez de herramientas y estándares, pero ya podemos decir que hay un base suficiente y estable. Vamos a ver algunas herramientas que podemos usar.

Registro de esquemas

El registro de esquemas nos permite compartir los esquemas de los metadatos y datos de los eventos entre productores y consumidores, sirviendo además como validadores.

El registro de esquemas es una herramienta muy usada habitualmente, pero que presenta el gap de que no ofrece contratos completos realmente, ya que no proporciona información sobre los canales o tópicos y, en muchas ocasiones, incluso deberíamos consultar varios esquemas para saber la estructura completa de un evento o mensaje:

Registro de esquemas

Dentro de los registros de esquemas típicos podemos encontrar Apicurio y Confluent.

AsyncAPI

Por si no lo conoces, AsyncAPI es un proyecto que nace para describir las APIs asíncronas, sean eventos o no, de forma similar a como describimos las APIs síncronas con OpenAPI. No voy a entrar en detalles de la especificación porque está muy bien explicado en la página oficial. Simplemente, me gustaría comentar que AsyncAPI se ha convertido en el estándar de facto para describir los contratos de las APIs asíncronas.

Podemos utilizar AsyncAPI para definir los contratos de los eventos que se intercambian entre los servicios, referenciando los esquemas publicados en el registro de esquemas y, de esta forma, poder proporcionar contratos completos:

Utilizamos AsyncAPI para definir los contratos de los eventos

A partir de esta definición, se puede incluso generar mocks para poder desarrollar de forma independiente los consumidores.

¿Por qué referenciar los esquemas en vez de crear documentos de AsyncAPI autocontenidos?

Algunas razones para ello son:

Event Catalog

Una vez que podemos definir los contratos de forma completa, nos queda el gap de cómo publicarlos de forma amigable y fácil para que puedan ser consultados por cualquier persona de la organización.

EventCatalog es un proyecto abierto que nos puede ayudar a documentar tanto nuestros eventos como los servicios que los producen o consumen como los dominios a los que pertenecen. Podemos utilizarlo de varias maneras, desde incluir nosotros toda la información a utilizar las definiciones de AsyncAPI para que alimenten el portal. Para ello, es necesario instalar el plugin de integración con AsyncAPI.

EventCatalog nos ayuda a documentar eventos y servicios

En el momento de desarrollo de este post, Event Calaog no soporta documentos AsyncAPI con esquemas en AVRO. Esta incidencia está pendiente de resolución.

Apicurio, que lo hemos nombrado como registro de esquemas, permite importar documentos AsyncAPI y utilizarlo como API Portal.

Cerrando el círculo

Finalmente, vamos a ver cómo podemos integrarlas todas para poder disponer de una solución end-to-end incorporándose dentro de nuestro proceso de CI/CD. La siguiente imagen muestra cómo sería:

Proceso de CI/CD

De esta manera, cuando un desarrollador sube un esquema o un contrato AsyncAPI al repositorio de código, el proceso de CI/CD puede publicarlo en el registro de esquemas o en el Portal para que cualquier persona de la organización pueda disponer del contrato de forma inmediata.

Conclusión

En este post hemos abordado la mayoría de los principios básicos dentro de las arquitecturas EDA, a nivel teórico, desde el diseño de eventos y tópicos hasta un vistazo a las herramientas más habituales. Espero que haya sido de utilidad. En posts posteriores, aterrizaremos todos estos conceptos y principios a nivel práctico, mediante un caso de uso.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.