Command Query Responsibility Segregation

2023-04-10

Desarrollo

Patrón Command Query Responsibility Segregation (CQRS) con un ejemplo.

Primero, voy a dar una definición concisa del patrón y qué problema soluciona, luego voy a tratar de exponer sus pros y sus contras, y al final voy a comentar un ejemplo pequeño que utilice hace ya unos años de este patrón.

El patrón Command Query Responsibility Segregation (CQRS) cómo su nombre bien lo índica, se trata de segregar o separar las responsabilidades en nuestros servicios, es decir, separar las operaciones CRUD (Create, Read, Update, and Delete). Por un lado, tenemos los comandos de creación, actualización y borrado, o si quieres las CUD en un servicio dedicado, con una base de datos dedicada, y por el otro las operaciones de lectura o queries en otro servicio dedicado, con otra base de datos, la R, para seguir el mismo hilo. Entonces, en corto y preciso, CQRS lo que nos propone es tener servicios separados para hacer todas las operaciones que actualicen data y por otro las que leen data.

Ok, pero por qué queremos hacer algo así?, o la pregunta real, ¿en qué caso hacemos algo así? Bueno, pensemos en una aplicación construida sobre una arquitectura de microservicios. En este escenario vamos a tener operaciones para crear recursos dentro de nuestra aplicación, como por ejemplo crear una orden de compra, crear una hoja de ruta para rastrear un envío de un paquete o crear una tarea en jira; pero también tenemos operaciones de lectura, como leer el estado de la orden, ver en que punto de la hoja de ruta se encuentra nuestro envío y así.

Estas operaciones de lectura en un monolito son bastante simples porque toda la data está contenida en el monolito y por lo general en una sola base de datos, pero en una arquitectura de microservicios es diferente porque la data puede estar dispersa en varios microservicios y en diferentes bases de datos. Es aquí donde se pone compleja la cosa, porque capaz la data está contenida en dos servicios, pero puede estar dispersa en 8 servicios o más, qué sé yo, dicho sea de paso, hay que aclarar que en una aplicación así seguramente la data es eventualmente consistente entre todos los servicios, que agrega aún más complejidad.

Bueno, este problema que acabo de relatar en el párrafo anterior lo resuelve el CQRS, ojo, no es el único patrón que existe ni tampoco es a prueba de balas, tiene sus pro y sus contras. La mayor contra que le veo es que agrega complejidad desde el punto de vista operacional porque tienes un servicio más que mantener y que es complejo de implementar, no todos los queries son sencillos y mantener la data de forma consistente puede ser un dolor de cabeza. Por otro lado, es que te da flexibilidad porque puedes escoger la tecnología adecuada para tus queries, por ejemplo si tienes que buscar sobre texto puedes usar un Elasticsearch en un servicio independiente, si tienes que hacer búsquedas geo espaciales? (esa es la palabra correcta?) puedes usar una base de datos que te brinde facilidades para ese tipo de queries; otro pro es que separa las responsabilidades en tus servicios permitiendo a un servicio dedicarse a una sola cosa.

Bueno, ahora un ejemplo rápido que utilice una vez. Recuerdo que habíamos desarrollado una aplicación para mostrar reportes sobre unas métricas de rendimiento a los clientes de la compañía. Teníamos proveedores que nos entregaban estas métricas mediante servicios externos, algunos eran Rest APIs, otros empleaban FTP. En los reportes teníamos data de todos los clientes en todos los países en los que operaban mezclada, por ejemplo, el cliente A y el cliente B en Perú tenía ciertas métricas, en Canadá tenía otras, en Estados Unidos otras, y todo eso estaba contenido en el mismo reporte el mismo reporte sin orden especifico.

Entonces había un monolito que procesaba esos reportes periódicamente y los presentaba en un dashboard, a esto le agrego que para mostrar ese dashboard se hacían cálculos sencillos pero que con mucho volumen de datos se volvía costosos. La figura muestra la arquitectura del monolito.

sistema de reportes monolito

El usuario iniciaba sesión mediante el DashboardController y este servía la vista, luego de forma asíncrona le pegaba al ReportController para generar las gráficas y tablas dentro del dashboard. El SupplierAPIClient era una interfaz con varias implementaciones para cada proveedor, claro, el diagrama está simplificado, pero creo que se entiende la idea. El ReportService invocaba la implementación del SupplierAPIClient correspondiente a cada proveedor para procesar y guardar datos en la base mediante el ReportDAO.

Al principio todo bien porque la cantidad de datos y de usuarios era poca, pero con el tiempo la solución se fue quedando pequeña y a empezar tener problemas de rendimiento. Entonces bueno, esto había que resolverlo, ¿Qué fue lo que hicimos? Aplicamos CRQS, en ese momento ni sabía que eso tenía nombre y los microservicios no era tan populares. Separamos el procesamiento de datos del servicio que hacía el query para el dashboard, fácil.

Primero sacamos la parte que procesaba los datos periódicamente a servicio aparte, llamado MetricsService, este leía los datos de todos los proveedores y los guardaba en una base de datos relacional; además le agregamos una API para que un administrador pudiese ejecutar este proceso si era necesario. Esta es la parte del comando o los CUD el patrón CQRS.

Luego tomamos el dashboard y lo convertimos en una SPA hecha en Vue (ni la toqué, no tengo idea de como funciona el dashboard ni como funciona Vue). Por último creamos un servicio que usaba una base de datos NoSQL (mongo) donde estaba contenido toda la data necesaria para mostrar en el dashboard, llamémoslo DashboardMetricsService, y era llamado por el MetricsService al final de su ejecución para así poder mantener la data consistente, o bueno, eventualmente consistente.

La figura de abajo muestra la arquitectura final con todos los "microservicios".

sistema de reportes distribuido usando CRQS

¿Qué ganamos con esto? Primero, como el procesamiento de datos no se hacía "on-the-fly" sino que se procesaban de antemano, el performance mejoro mucho. Segundo, separando la parte del sistema que se conectaba con las APIs de los proveedores, de la que mostraba esos datos a los clientes teníamos más flexibilidad y mantenibilidad. Tercero, facilidad para variar o cambiar la base de datos donde almacenábamos las métricas para mostrar a clientes si en algun momento surgia un requerimiento que ya no pudiesemos cumplir con Mongo, y cuarto, permitíamos a los clientes generar reportes más personalizados porque Mongo tiene ciertas bondades que una relacional no tiene, como búsqueda por ubicación. Entonces creo que si ganamos algunas cosas además de un poquito de experiencia.

Espero que hayas disfrutado el artículo.