En el anterior post sobre CQRS vimos que existían muchas formas de implementar este estilo arquitectónico, ya que había que tomar varias decisiones de diseño, principalmente que paradigma de persistencia íbamos a usar en el subsistema de comandos y en el de consultas, el método de sincronización entre éstos y el nivel de consistencia que nos interesa. En este post nos centraremos en la sincronización.
¿Qué sincronizamos?
Dejando de lado la tecnología concreta que pudiéramos usar para sincronizar ambos subsistemas, nos surge una pregunta interesante: cuando decimos que ambos subsistemas se sincronizan, ¿cual es exactamente la información que se intercambia entre ambos? ¿Qué es lo que enviamos y recibimos a través de ese mecanismo de sincronización? Si no queremos darle muchas vueltas, podemos simplemente hacer que el nuevo estado, resultante de una operación de negocio, sea recibido por el subsistema de consulta. Es una solución sencilla y directa, pero tiene sus problemas:
- Transmitir el estado completo una y otra vez puede ser ineficiente.
- Una de las ventajas de CQRS es precisamente que ambos susbsistemas pueden tener modelos de información diferentes, uno optimizado para operaciones y otro para consultas. Si transmitimos el estado del modelo del subsistema de comandos, la transformación de este en el modelo de información de consulta puede ser costosa e implicar una lógica compleja.
Otro enfoque al problema de la sincronización es transmitir sólo los cambios en el estado del sistema, y no el nuevo estado al completo. De esta forma nos evitamos enviar bastante información, con lo que solucionamos el primer problema. Ummm, dos subsistemas que se sincronizan enviando únicamente los cambios de estado, esto suena sospechosamente a eventos. Técnicamente sólo serían eventos si decidimos que nuestra sincronización siga un paradigma «push» (publish/subscribe). Pero alternativamente podríamos usar un paradigma «pull», donde el subsistema de consulta pregunta al de comandos qué cambios se produjeron desde un determinado momento. En este caso alguno podría argumentar que no son estrictamente «eventos» sino «cambios». Yo, por simplicidad, hablaré siempre de eventos, tanto en el caso de «pull» como de «push».
Ya estoy viendo que a más de uno le ha hecho «click» en la cabeza, y ha visto la luz: «claro, ahora si cambio la propiedad en una entidad, emito un evento «property change», y si creo una instancia emito un evento de «create», con lo cual usando el «framework X» lo tengo resuelto y …» Yo mismo he pensado esto en su momento, pero claramente este enfoque nos lleva al lado oscuro del CRUD, que siempre está ahí para tentarnos. A este enfoque lo llamo usar eventos CRUD, y no lo recomiendo en general para CQRS. El problema es que el subsistema de consulta debe ser capaz de interpretar los eventos de la forma más desacoplada posible del subsistema de comandos, pero los eventos CRUD están definidos en función del esquema de datos del subsistema del comandos. Si al subsistema de consultas le llega un evento «property change de la propiedad P sobre la entidad E», éste debe conocer el esquema de datos del subsistema de comandos para poder interpretarlo. Si hiciéramos un cambio en dicho esquema, tendríamos seguramente que modificar el código en el subsistema de consulta, aunque no se haya producido ninguna modificación en la lógica de negocio. Es más, es probable que haya eventos que ya no tengan sentido, al desaparecer alguna entidad o propiedad. Lo que se intenta con CQRS es que ambos subsistemas puedan evolucionar por separado, y para ello necesitamos que el acoplamiento entre ambos sea bajo. Por lo tanto evitad los eventos CRUD, excepto en el caso que tengáis una necesidad real del mismo esquema de datos en ambos subsistemas (con lo que CQRS no sería una solución tan ventajosa).
Lo que necesitamos son eventos de negocio, que representen cambios en el estado del modelo de negocio, no en el esquema de datos. Para averiguar que eventos de negocio tenemos, debemos hacer antes un buen análisis de la funcionalidad de negocio. Esto es consistente con CQRS, ya que de todas maneras debemos averiguar que operaciones de negocio (comandos) tenemos, por que estados pasa el sistema, etc. Por ejemplo, en una tienda electrónica debemos modelar el ciclo de vida de un pedido. Podemos llegar a algo como esto:
Existiría un comando «Checkout» que una vez ejecutado por el subsistema de comandos, podría hacer evolucionar el estado del pedido desde «Open» a «Accepted«, o en el caso de un problema, tal vez de pago, al estado «Rejected«. Estas transiciones de estado generarían respectivamente los eventos de negocio «OrderAccepted» y «OrderError«. Ambos eventos transportarían el identificador de pedido y quizás algún campo auxiliar, como por ejemplo, una razón para el rechazo del pedido. Obviamente hay bastantes comandos, estados y eventos, y seguramente falten cosas, pero el hacer un pedido implica un proceso de negocio no trivial.
De esta forma, el acoplamiento entre ambos subsistemas se basa sólo en el formato de los eventos, y en compartir un modelo de negocio común, pero el subsistema de consultas no tiene porque saber como se han estructurado las tablas o los objetos en el subsistema de comandos. El ciclo de vida de un pedido va a cambiar sólo cuando cambie el negocio, no por cualquier detalle técnico, por lo tanto constituye un contrato más estable que un esquema de datos.
El lector avezado habrá notado que esto está muy alineado con el enfoque Domain Driven Design (DDD). Operaciones de negocio, eventos de negocio, estados por los que evoluciona un pedido, etc. Todo esto está muy relacionado con el concepto de lenguaje ubicuo de DDD, no es de extrañar que CQRS sea una arquitectura a la que se suele llegar aplicando la metodología DDD, y viceversa, forzándote a hacer CQRS es fácil que termines haciendo DDD.
Conclusión: una forma muy apropiada de sincronizar ambos subsistemas es mediante eventos de negocio.
¿Cómo sincronizamos?
Desde el punto de vista tecnológico, lo primero a decidir es si queremos un mecanismo de notificación «pull» o «push».
El paradigma «pull» se basa en que el cliente pregunta al servidor para obtener los datos. Como estamos hablando de pedir cambios, el cliente debe hacer «polling», es decir repetir la consulta cada cierto tiempo. Esto en algunos casos es ineficiente, ya que muchas veces el cliente va a preguntar en vano, ya que no hay cambios que notificar. De esta manera vamos a estar preguntando y consumiendo recursos para no conseguir nada. Sin embargo en algunos casos es ventajosos usar este paradigma:
- Es muy simple, no se necesita ningún producto ni protocolo sofisticado para llevarlo a cabo.
- Internet y la web están diseñados para el paradigma «pull». Por lo tanto están optimizados para este tipo de interacción, en concreto el uso correcto de las caches, las peticiones HTTP condicionales y el protocolo ATOM/RSS, nos permiten implementar este enfoque de forma eficiente.
- Es muy interoperable.
- El servidor (en este caso el subsistema de comandos) está totalmente desacoplado de los clientes. No necesita saber cuantos clientes tiene, ni que eventos recibió cada cliente, ni nada. Simplemente responder a las consultas. Es decir podemos implementar un servidor stateless, que es más sencillo y escalable.
Por lo tanto en algunos escenarios es bastante interesante usar este enfoque, por ejemplo:
- El subsistema de consulta y el de comandos se comunican a través de la web. Tal vez ambos están en diferentes máquinas en la nube (ideal para alta disponibilidad). En este caso se puede usar HTTP con ATOM/RSS para distribuir los eventos mediante una interfaz REST.
- Una tercera parte ha implementado una aplicación de agregación de información que usa nuestros eventos. Tal vez sea un business partner, otra empresa de nuestro grupo, el departamento del edificio de al lado, o quizás la nuestra sea una startup que quiere ofrecer una API. De nuevo lo lógico es publicarlo mediante una API REST, que es inherentemente «pull».
En general todos estos escenarios implican una alta latencia, lo que provoca que las consultas puedan estar segundos o minutos atrasadas con respecto al estado real del sistema.
El paradigma «push», donde el cliente se subscribe al servidor y recibe los eventos sólo cuando ocurren. El cliente no necesita preguntar al servidor periódicamente, es notificado cuando es necesario. La principal desventaja de este sistema es el acoplamiento entre el servidor y el cliente. El servidor debe mantener una lista de subscripción de forma persistente, y almacenar que eventos fueron enviados a que clientes. Esto puede producir un problema de escalabilidad si tenemos un número ilimitado de clientes. También complica el diseño del subsistema de comandos. En general esto se solventa usando mecanismos de mensajería como JMS o AMQP. También se pueden usar productos de middleware, por ejemplo un producto de Enterprise Service Bus.
Algunos escenarios:
- El subsistema de consulta y el de comandos están en el mismo proceso. Nos basta con implementar un patrón «Observer».
- Ambos subsistemas están en la misma máquina o en el mismo CPD. Usaríamos un sistema de mensajería o un Enterprise Service Bus
En resumen, existen a nivel tecnológico muchas variantes y soluciones para implementar el mecanismo de sincronización. Lo importante es que transmitamos eventos de negocio mediante este.
En el próximo post nos adentraremos en el mundo del subsistema de comandos.
Sincronización pull con el estado del negocio a traves de un API REST de eventos de dominio!! Toma ya !!! #LaVidaPuedeSerMaravillosa
Algo que suele preocuparme cuando pienso en modelos orientados a eventos es la necesidad de sincronismo en algunas operaciones. Por ejemplo, en el modelo que comentas en el caso de que una orden sea rechazada … ¿como trasladarías esta información (importante) al usuario en un modelo de eventos típicamente asíncrono? … o en caso de implementar sincronía ¿hasta donde extender la validación del comando antes de dar una respuesta?