Julio de 2016

Volumen 31, número 7

CQRS: Aprovechar CQRS para crear sistemas con alta capacidad de respuesta

Por Peter Vogel | Julio de 2016

El patrón de separación de responsabilidad de comandos y consultas (CQRS) ha adquirido una mayor popularidad en los últimos tres o cuatro años. Definitivamente, se trata de una herramienta esencial en escenarios de colaboración, en los que hay un conjunto de datos que actualizan varios procesos (Dino Esposito explica un caso para usar CQRS de una forma aún más amplia en su columna de Tecnología de vanguardia de junio de 2015, "CQRS para la aplicación común", que puede encontrar en bit.ly/1OtQba3). Yo iría más allá y afirmaría que, de hecho, CQRS es el patrón de diseño predeterminado para los desarrolladores de ASP.NET MVC que consultan datos para visualizarlos en sus vistas y después emiten comandos para actualizar tablas cuando los datos se envían de vuelta a los controladores de MVC.

Sin embargo, CQRS es una táctica que debería aplicarse como parte de una estrategia mayor. El primer paso de esa estrategia es el diseño guiado por el dominio (DDD), el cual se describe en la columna de junio de 2013 de Julie Lerman, "Reducción de los modelos de EF con los contextos limitados de DDD", que puede encontrar en bit.ly/1TfF7dk. DDD lleva a dividir la aplicación en dominios cooperativos, cada uno de los cuales puede incluso tener su propia base de datos, además de por supuesto su modelo de negocio dedicado. DDD ofrece estrategias y tácticas que permiten que los dominios se desarrollen independientemente los unos de los otros mientras siguen funcionando juntos.

Definición de dominios de inventario

Pero DDD solo elimina potencialmente la necesidad de colaboración. Considere, por ejemplo, una aplicación de ventas en línea. En ese caso, existen unos datos compartidos que son críticos: los niveles de inventario. Para garantizar que el negocio no intenta vender algo de lo que no dispone, puede mantener recuentos de inventario precisos de cantidades disponibles (Quantity on Hand, QoH) de cada referencia de almacén (SKU), o puede disponer permanentemente de un inventario extra "por si hiciera falta". En el mundo austero actual esa segunda opción no se tiene en cuenta: las empresas no quieren mantener un inventario mayor del necesario.

La actualización de las QoH en función de las transacciones del negocio es más complicada de lo que cabría pensar, ya que un sistema de inventario del mundo real controla una amplia variedad de transacciones. Las transacciones más evidentes por supuesto son el decremento de la QoH cuando se vende una SKU, y el incremento de la QoH cuando se reciben nuevas SKU. Además, una empresa realizará de forma periódica un "recuento de inventario" para determinar la QoH real de cada SKU. Como incluso en un sistema correctamente administrado la precisión del inventario no es del cien por cien, ese recuento requerirá un cambio en los niveles de inventario. Por si fuera poco, en ocasiones se detectan SKU que tienen algún tipo de desperfecto y se eliminan del inventario. A veces, después de que una SKU se venda y se elimine del inventario, el cliente cancela el pedido y la SKU se devuelve al almacén.

Las empresas quieren realizar un seguimiento de todas estas distintas transacciones y mantener la información específica de cada una. Por ejemplo, cuando se reciben nuevas SKU, las empresas quieren conocer qué factura se ha utilizado para comprar las SKU; cuando se detecta que hay SKU defectuosas, las empresas quieren saber por qué; y, durante la comprobación del inventario, las compañías quieren conocer cómo de grande es la discrepancia. El departamento de contabilidad necesita esta información para realizar informes precisos sobre el "estado de la empresa"; el departamento de operaciones la necesita para planificar correctamente de cara al futuro. Debido a la necesidad de esta información adicional, estas transacciones no se pueden tratar como meras adiciones o eliminaciones en el inventario.

No obstante, no todas estas transacciones pertenecen al mismo dominio. Estas transacciones se dividen en dominios denominados "ventas", "contabilidad", "operaciones", "recepción", etc. La división de las transacciones en dominios refleja la realidad de que dominios distintos tienen diferentes requisitos.

La mayoría de dominios, por ejemplo, no necesitan datos actualizados al minuto (incluso aunque tuvieran un retraso de un día laboral con respecto a las transacciones reales, no sería un problema). El departamento de contabilidad, por ejemplo, solo necesita conocer el estado financiero del inventario a final de mes; e incluso entonces podrían no tener expectativas de disponer de esa información hasta los primeros días del mes siguiente. Aunque podría ser posible mantener los datos de inventario más actuales, sería complicado encontrar una justificación de negocio para hacerlo. La información de inventario de esos departamentos puede ser "coherente al cabo de un tiempo".

Sin embargo, el sistema de ventas no puede tener información de inventario "coherente al cabo de un tiempo". El departamento de ventas necesita conocer cuál es la QoH en este mismo momento, de forma que pueda decidir si una SKU se puede mostrar al cliente ("Solo quedan 2 en stock. Compre ahora."). De hecho, aunque la mayoría de dominios tendrían un único número para la QoH de una SKU, el sistema de ventas podría mantener la QoH como dos números. Un número es la cantidad "reservada" (las SKU solicitadas por un usuario que está en proceso de crear un pedido) y el segundo número es la cantidad "aún disponible para la venta". Si un cliente compra dos elementos, el número reservado se incrementa en dos unidades y el número de disponibles para la venta se reduce en dos; al final de la venta, o bien la cantidad reservada se reduce en dos unidades, o si el usuario cancela el pedido, se agregan de vuelta al número de disponibles para la venta.

Tanto el departamento de contabilidad como el de operaciones necesitarán la flexibilidad de una base de datos relacional para unir tablas de una variedad de formas. También necesitarán la capacidad de buscar esos datos, en ocasiones de formas que no se habían considerado antes de detectar un problema concreto. Debido a la cantidad de datos relacionados y a la necesidad de explorar el historial de transacciones, también será necesario usar paginación.

El sistema de ventas no requiere tanta flexibilidad. Las relaciones entre entidades están fijadas con el diseño de la interfaz de usuario, como lo están los requisitos de búsqueda (aunque la compatibilidad con la paginación sigue siendo necesaria).

Los requisitos de tiempos de respuesta también varían según el dominio. En la mayoría de departamentos, un tiempo de respuesta medido en segundos no sería un problema para la empresa; para el sistema de ventas, el tiempo de respuesta se debe medir en fracciones de un segundo.

La creación de un único sistema que cubra todas estas necesidades sería complicado (yo diría que imposible). La creación de una aplicación para cada dominio es, cuando menos, posible. Por ejemplo, el departamento de administración de productos tendría una lista de productos que se actualizaría constantemente tanto con nuevos productos como con información sobre productos ya existentes; el dominio de ventas podría, por otro lado, mantener una lista de productos de solo lectura o solo consulta que se sincronizase de manera regular con los datos del dominio de administración de productos.

Piense en los dominios como el principio de responsabilidad única aplicado a nivel de empresa. Cada dominio controla una parte del negocio correctamente. Aunque la empresa sea complicada, cada dominio puede ser (relativamente) simple.

La solución con CQRS

En cualquier caso, todos estos dominios siguen compartiendo los niveles de inventario. A medida que las transacciones pasan a través de dominios como contabilidad y recepción, deben notificar al sistema de ventas los cambios en los niveles de inventario. Incluso dentro del sistema de ventas, varios clientes podrían estar intentando comprar las mismas SKU, modificando individualmente los niveles de stock hacia arriba y abajo, y requiriendo algún nivel de bloqueo a medida que dichos números se ajustan.

El patrón CQRS resulta útil aquí, ya que va más allá de lo que el desarrollador de ASP.NET MVC típico consideraría. Dentro de la mayoría de dominios, por ejemplo, las aplicaciones pueden realizar consultas a sus propias bases de datos, que contienen la información que el dominio necesita. Cuando es momento de emitir un comando para ajustar los niveles de inventario, todos los dominios deben actualizar los datos del dominio de ventas en línea. Y la obligación va en ambas direcciones: a medida que se venden elementos, el sistema de ventas debe notificar a contabilidad, operaciones y a otros dominios sobre los cambios en las QoH debidos a las ventas de cada SKU.

Sin embargo, en lugar de actualizar los datos de otro dominio, cada dominio solo está obligado a notificar a otros dominios sobre algo que sea del interés de esos otros dominios (en este caso, las QoH). Cada dominio debe ser responsable de actualizar sus propios datos, ya que cada dominio conoce cómo administrar sus datos y ningún otro dominio lo sabe.

Como ejemplo, el dominio de operaciones está constantemente explorando las relaciones entre sus datos a fin de predecir demandas de inventario y de determinar lo que provoca las fluctuaciones del nivel de stock. Ese dominio debe admitir la flexibilidad en la consulta de datos que una base de datos relacional tradicional ofrece. La complejidad en el dominio de operaciones viene dada por el tipo de análisis necesario en ese dominio.

El dominio de ventas, por otra parte, necesita algo más simple. Necesita conocer cuál es la QoH (reservada y disponible para venta) de cualquier SKU. Incluso podría tener sentido que para el sistema de ventas solo se mantuviera el identificador de cada SKU y sus dos números de QoH constantemente en memoria. Si eso no es posible por el número de elementos del inventario, también tendría sentido mantener en memoria el 20 por ciento del inventario que produce el 80 por ciento de la actividad de ventas de la compañía. Los otros elementos del inventario se podrían mantener en alguna base de datos NoSQL que estuviera diseñada para dar soporte a las transacciones de ventas, sin necesidad de ofrecer la flexibilidad que requiere, por ejemplo, el dominio de operaciones. La complejidad en el dominio de ventas viene dada por la necesidad de disponer de tiempos de respuesta bajos.

Estas diferencias implican que no se puede esperar que el dominio de operaciones conozca cómo actualizar los números de QoH del dominio de ventas (y viceversa, por supuesto).

Por lo tanto, los dominios podrían estar realizando consultas en una base de datos (la suya propia) mientras que envían comandos a otra (todos envían actualizaciones de QoH al dominio de ventas, por ejemplo). Aunque DDD ofrece una estrategia para segmentar dominios con distintos requisitos empresariales, CQRS ofrece una de las tácticas para administrar actualizaciones entre esos dominios (para ver una explicación más en profundidad sobre el lado de consultas de CQRS, consulte la columna de marzo de 2016 de Esposito, "La pila de consultas de una arquitectura CQRS" en bit.ly/1WzjvPi).

Control de comandos y eventos

Por supuesto, no querrá que las aplicaciones de estos dominios sean más complicadas al tener que tratar con la diversidad de dominios a los que se debe notificar por cada transacción. En lugar de mantener un seguimiento de todos los dominios que se deben actualizar, cada aplicación enviará transacciones a una utilidad que es responsable de notificar a los distintos dominios (normalmente se conoce como "bus de comandos"). Cuando se definan nuevos dominios (o que los dominios existentes cambien sus solicitudes), solo deberá actualizarse el bus de comandos dentro del dominio que origine la transacción para reflejar las nuevas notificaciones que son necesarias.

Estas transacciones se pueden dividir en categorías: comandos y eventos. La distinción entre las dos es más conceptual que técnica. En efecto, tanto los comandos como los eventos son mensajes que encapsulan la información clave sobre una transacción. Para nuestras transacciones de inventario, esa información sería el identificador de la SKU, el cambio neto en el nivel de inventario y los datos adicionales necesarios para la transacción (cuando se reciben bienes, esos datos adicionales pueden ser el número de proveedor y el número de factura; durante una comprobación de inventario, los datos adicionales podrían ser el identificador del empleado que hizo el recuento de las SKU). Estos mensajes se podrían codificar como objetos POCO o como documentos XML/JSON (o ambos, en función de cómo se envíen los datos entre dominios).

Para mí, la definición de un comando es que es algo dirigido a un único receptor para llevar a cabo una tarea. Un comando normalmente es una tarea que debe realizarse inmediatamente y que, obviamente, se envía antes de que se lleve a cabo la tarea. También puede esperarse que un comando devuelva una respuesta que indique si se ha realizado de forma correcta o errónea, que la aplicación puede usar para informar al usuario de si ha funcionado (y, potencialmente, provocar que la aplicación realice una consulta para recuperar los datos que muestran los resultados que se consiguieron). La mayoría de actualizaciones dentro del dominio que originó la transacción probablemente se controlen con comandos.

Los eventos, por otra parte, se producen después de que se realice la tarea, se pueden procesar mediante varios receptores y normalmente no es necesario que se procesen inmediatamente. No se espera que los eventos devuelvan un resultado, al menos inmediatamente. Si hay algún problema con un evento, la aplicación normalmente lo descubrirá a través de algún mensaje de devolución diferido ("Lo sentimos, pero no es posible procesar el pedido porque su tarjeta de crédito ha sido rechazada"). La mayoría de actualizaciones (pero no todas) fuera del dominio que originó la transacción probablemente se controlen con eventos.

Y, como en la mayoría de distinciones conceptuales, probablemente se trata de un continuo; algunos mensajes son "obviamente" comandos, algunos mensajes son "obviamente" eventos y hay algunos sobre los que personas sensatas podrían no estar de acuerdo.

Una única transacción en un dominio podría generar una combinación de comandos y eventos. Considere que nuevas SKU llegan a la zona de recepción. Cuando las SKU se han recibido correctamente, el bus de ese dominio enviaría un comando al sistema de ventas para que la QoH de esa SKU se incrementase inmediatamente; el bus también publicaría un evento de forma que los sistemas de contabilidad y operaciones recibieran una notificación de que "algo ha pasado" y debe tenerse en cuenta al final del mes. Si se observan los mensajes relacionados, podría ser complicado determinar cuál es el evento y cuál el comando (excepto, quizá, si se mira el nombre del mensaje); los eventos suelen tener nombres en tiempo pasado (GoodsReceived), mientras que los comandos suelen tener nombres en imperativo (IncreaseInventory).

El bus podría enviar el comando al sistema de ventas mediante una llamada a un servicio RESTful de ese dominio para su ejecución inmediata; el evento incluso se podría escribir en alguna cola de mensajes para que otros dominios lo procesaran cuando fuera más conveniente para ellos (he comentado algunas de las opciones en un artículo que escribí para VisualStudioMagazine.com, "Simplifying Applications by Implementing Eventual Consistency with Domain Events" [Simplificar aplicaciones mediante la implementación de coherencia final con eventos de dominio], que puede encontrar en bit.ly/1qn1wwV).

Por supuesto, incluso con el comando enviado al servicio web, ¿quién sabe lo que sucede detrás del servicio web? Para controlar grandes volúmenes de solicitudes simultáneas, el servicio web del dominio podría simplemente escribir el mensaje del comando en una cola y devolver una respuesta de tipo "Recibido, gracias", lo que mantendría el tiempo de respuesta reducido y mejoraría la escalabilidad. Además de mejorar la escalabilidad, la escritura de comandos en una cola permite que el dominio se recupere de lo que de otro modo serían problemas catastróficos. Por ejemplo, si la base de datos o la red no están disponibles, el sistema de ventas puede esperar pacientemente a que el servicio se restaure y después procesar los comandos que esperan en su cola. Por tanto, incluso los comandos pueden terminar en colas.

Como he mencionado, la distinción entre los comandos y los eventos es conceptual, no es técnica.

Procesamiento de comandos y eventos

Gracias a CQRS, las aplicaciones ahora pueden trabajar con una combinación de dos bases de datos: una para las consultas (probablemente local en el dominio) y otras bases de datos que sean los destinos de comandos y eventos. Por ejemplo, el sistema de ventas trabajará con un almacén de datos que incluye la base de datos NoSQL que contiene los datos de QoH; las aplicaciones de contabilidad y operaciones podrían trabajar con un almacén de datos organizado en torno al historial de eventos y comandos.

La diferencia entre los dos sistemas es que el sistema de ventas necesita una instantánea del estado actual de los niveles de inventario para cubrir las solicitudes de tiempo de respuesta; los dominios de contabilidad y operaciones necesitan un historial de lo que le ha sucedido a cada SKU para permitir el análisis. Los dominios de contabilidad y operaciones pueden funcionar con una táctica distinta: el origen de eventos. Con el origen de eventos, la lógica de dominio recorre el registro de auditoría de los eventos que se le han notificado para proporcionar una respuesta final (para el sistema de contabilidad podría ser "En función del historial de transacciones enviadas, el valor actual del inventario es X dólares").

El origen de eventos presenta ventajas e inconvenientes. Con el origen de eventos siempre es posible recrear una instantánea del estado actual de los datos mediante un reprocesamiento de la lista de transacciones; el departamento de contabilidad agradece esa característica ya que agrega ajustes a la lista de eventos. Con el origen de eventos, también es posible describir el futuro mediante el procesamiento de eventos potenciales (ventas y entregas esperadas); el departamento de operaciones agradece esa característica durante la planificación.

Sin embargo, a medida que crece la lista de eventos, también lo hace el tiempo de respuesta. El departamento de contabilidad puede recorrer hacia delante desde su "último estado correcto conocido" (probablemente los números de cierre del mes anterior) y generar una instantánea que represente los números de final de mes. Esa instantánea se guarda como el "último estado correcto conocido" actual y se publica como los informes de final de mes. Las operaciones se pueden recorrer hacia delante desde hoy a un punto indeterminado en el futuro y, probablemente, nunca generar una instantánea; recrearían el futuro cada vez que se solicitase. Dadas las expectativas en tiempo de respuesta de estos dominios, probablemente sean escenarios lógicos.

No obstante, para determinar las QoH actuales del sistema de ventas mediante origen de eventos, el sistema de ventas tendría que recorrer hacia adelante todas las transacciones desde el último recuento de inventario. Como los recuentos de inventario suponen un esfuerzo humano considerable, no se realizan muy a menudo. Como resultado, el procesamiento de todos esos eventos desde el último recuento provocaría tiempos de respuesta inaceptables para el sistema de ventas. En su lugar, el sistema de ventas mantendría los números de QoH constantemente actualizados en memoria.

Resumen

Aunque las consultas requieren varios niveles de compatibilidad en la base de datos (y, como resultado, una variedad de índices y claves primarias y externas), las actualizaciones no. Prácticamente todas las actualizaciones se guían por los identificadores de las entidades relacionadas. La lista de las SKU del inventario con los números de QoH, por ejemplo, se guía completamente por los identificadores de SKU. Esto puede simplificar enormemente el modelo de datos del lado de comandos de un sistema CQRS. La capacidad de Entity Framework de generar una colección de elementos SalesOrderItems para un elemento SalesOrder no es pertinente si el mensaje de comando o evento simplemente incluye los identificadores de todos los elementos SalesOrderItems que se cambiaron en una transacción.

El impacto en el bloqueo de la base de datos como resultado de este diseño es interesante. Las actualizaciones en las QoH y las cantidades reservadas en el sistema de ventas consisten en la modificación de uno o varios de los valores enteros; el bloqueo debería ser mínimo. El bloqueo de algunos de los otros sistemas puede desaparecer si dichos sistemas siguen un origen de eventos; la transacción siempre inserta alguna transacción en una tabla de eventos, por lo que no hay actualizaciones.

Efectivamente, en ese caso el negocio tiene varios procesadores independientes que actualizan datos dentro de sus dominios, procesan comandos y eventos. Sin bloqueo, es posible que esto cree conflictos. Por ejemplo, un comando para comprar dos elementos podría aparecer al mismo tiempo que un evento que reduce la QoH a cero (alguien hizo un recuento de inventario y observó que no quedaba ninguna unidad). Curiosamente, un enfoque de origen de eventos basado en colas podría solucionar este problema; el procesador de actualizaciones de QoH del sistema de ventas podría funcionar en base al origen de eventos, recorriendo todos los comandos recibidos recientemente en una cola (dentro de algún límite) y actualizando la QoH con el total de sus resultados. Los comandos que se muestran de manera simultánea se resumirían en una única actualización. De forma alternativa, podría simplemente ser necesario reconocer que, en ocasiones, se permite que el negocio cancele un pedido de la misma forma que un usuario puede hacerlo.

CQRS es una potente herramienta. Sin embargo, ofrece su mejor versión cuando se aplica a almacenes de datos compartidos, procesos colaborativos y dentro de la estrategia que DDD proporciona.


Peter Vogeles arquitecto de sistemas y el director de PH&V Information Services. PH&V ofrece servicios de consultoría full-stack desde el diseño de la experiencia de usuario al diseño de la base de datos, pasando por el modelado de objetos. Puede ponerse en contacto con él en peter.vogel@phvis.com.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo: Dino Esposito y Julie Lerman
Dino Esposito es el autor de "Microsoft .NET: Architecting Applications for the Enterprise" (Microsoft Press, 2014) y "Modern Web Applications with ASP.NET" (Microsoft Press, 2016). Como experto técnico para las plataformas .NET y Android en JetBrains y orador frecuente en eventos mundiales de la industria, Esposito comparte su visión sobre el software en software2cents@wordpress.com y en su Twitter @despos.

Julie Lerman es una MVP de Microsoft, mentora y consultora de .NET que vive en las colinas de Vermont. Puede encontrarla haciendo presentaciones sobre el acceso a datos y otros temas de .NET a grupos de usuarios y en conferencias en todo el mundo. Su blog es thedatafarm.com/blog y es la autora de "Programming Entity Framework", así como de una edición de Code First y una edición de DbContext, de O’Reilly Media. Sígala en Twitter en @julielerman y vea sus cursos de Pluralsight en juliel.me/PS-Videos.