Skip to content

CQRS - Command Query Responsibility Segregation

CQRS es un patrón de arquitectura de software que se ha vuelto cada vez más prominente por su enfoque claro y estructurado para separar las operaciones de lectura y escritura dentro de una aplicación. Este concepto fue ampliamente difundido por Greg Young, basándose en el principio de Bertrand Meyer de que una función debería cambiar el estado de un objeto o devolver información sobre ese objeto, pero no ambas.

El patrón CQRS toma esta idea y la expande creando dos objetos separados: uno para actualizar datos (Command) y otro para leer datos (Query). Esto no solo alivia la carga cognitiva al trabajar con modelos de dominio complejos, sino que también permite una gran flexibilidad en cómo se implementan y optimizan las operaciones de base de datos.

Aplicación y Ventajas de CQRS

CQRS se usa típicamente en sistemas donde la complejidad entre las operaciones de dominio (es decir, la lógica de negocio) y las operaciones de datos (es decir, la base de datos) se ha vuelto engorrosa de manejar. Al separar las responsabilidades de escritura y lectura, cada lado puede ser optimizado independientemente. Las operaciones de escritura pueden ser diseñadas para garantizar la integridad transaccional y la coherencia del dominio, mientras que las operaciones de lectura pueden ser optimizadas para rendimiento y escalabilidad, a menudo utilizando diferentes modelos de datos que son más adecuados para consultas.

Uno de los principales objetivos de CQRS es mejorar la mantenibilidad y la claridad del diseño de software. Al dividir claramente las operaciones de comando y consulta, se facilita la lógica del negocio y se permite que cada parte del sistema evolucione a su propio ritmo. Esta separación también puede conducir a mejoras significativas en el rendimiento, ya que el almacenamiento y la infraestructura pueden ser personalizados para cada tipo de operación.

Implementación de CQRS

Implementar CQRS en un modelo de software puede ser tan simple o complejo como sea necesario para satisfacer los requisitos de la aplicación. En su forma más básica, CQRS puede ser implementado simplemente dividiendo las operaciones de lectura y escritura en diferentes clases o componentes. Por ejemplo, un sistema de comercio electrónico podría tener un servicio CreateOrderCommand para manejar la creación de pedidos y un servicio GetOrderQuery para recuperar información sobre un pedido en concreto.

  • DirectoryOrder
    • DirectoryApplication
      • DirectoryCommand
        • CreateOrderCommand.php
      • DirectoryQuery
        • GetOrderQuery.php
    • DirectoryDomain
      • Order.php Agregado de dominio que contiene otros agregados como el Customer, LineItems, etc.
      • LineItem.php
      • OrderRepository.php
    • DirectoryInfrastructure
      • InMemoryOrderRepository.php
      • DoctrineOrderRepository.php

Un paso más allá

A veces, cuando un sistema ha escalado mucho y comienza a tener problemas de rendimiento debido a una alta concurrencia, incluso contando con una base de datos para lectura y otra para escritura no resulta suficiente. En estos casos, solución común suele ser la de crear un modelo de lectura independiente del modelo de escritura. Imagina el caso en el que queremos recuperar las direcciones físicas de los clientes de los 100 últimos pedidos… la consulta a la base de datos tendría que hacer al menos un JOIN con la tabla de pedidos y otra con la tabla de clientes (y si tenemos las direcciones guardadas en una tabla a parte, se necesitarían 2 JOINS), lo que podría resultar en una consulta muy lenta dado que la tabla orders y customers a menudo suelen contar con muchos registros. En esta situación, puede ser justificable el crear un modelo de lectura que contenga la información necesaria para realizar la consulta de manera rápida, por lo que el modelo de lectura se puede actualizar de manera asíncrona cada vez que se crea un pedido (ambos deben de presentar la misma información), de manera que la consulta sea rápida y eficiente. El nuevo modelo de lectura puede ser implementado en forma de una nueva tabla en base de datos con los campos necesarios para una consulta sin JOINs, o incluso en un sistema de persistencia diferente como una base de datos NoSQL (MongoDB, ElasticSearch, etc).

Dos pasos más allá (microservicios)

En otras ocasiones la separación de responsabilidades entre los comandos y las consultas puede ser tan grande que se justifique la creación de dos sistemas completamente independientes. En estos casos, se puede optar por la creación de dos microservicios, uno para los commands y otro para las querys. De esta manera, se puede escalar cada uno de los sistemas de manera independiente, y se puede incluso optar por tecnologías diferentes para cada uno de los sistemas. Por ejemplo, el sistema de comandos podría estar implementado en PHP con una base de datos relacional, mientras que el sistema de consultas podría estar implementado en Node.js con una base de datos NoSQL.

Diez pasos más aquellas (event sourcing)

En ocasiones, la complejidad de los sistemas puede ser tan grande que se justifique la implementación de un sistema de eventos. En estos casos, en lugar de almacenar el estado actual de los objetos en la base de datos, se almacenan los eventos que han ocurrido en el sistema. De esta manera, se puede reconstruir el estado actual de los objetos en cualquier momento a partir de los eventos almacenados. Este sistema de eventos puede ser implementado en una base de datos relacional, en una base de datos NoSQL, o incluso en un sistema de mensajería como Kafka. En este tipo de sistemas, lo que se persisten son los eventos que han ocurrido en el sistema. Por ejemplo, en lugar de guardar el estado actual de un pedido, se guardan los eventos que han ocurrido en el pedido (pedido creado, pedido pagado, pedido enviado, etc). De esta manera, se puede reconstruir el estado actual del pedido en cualquier momento a partir de los eventos almacenados. Otro ejemplo más facil de ver es el de un cajero de un banco, en lugar de guardar el saldo actual de la cuenta, se guardan los eventos que han ocurrido en la cuenta (ingreso de 100€, retirada de 50€, etc). De modo que cuando se solicite saber el saldo actual de la cuenta, se pueden reconstruir a partir de los eventos almacenados.

Desafíos e Inconvenientes

A pesar de sus ventajas, CQRS introduce cierta complejidad en el sistema. Requiere una cuidadosa consideración y diseño para implementar efectivamente los modelos de lectura y escritura separados. Los equipos deben gestionar dos representaciones diferentes del mismo dominio y asegurarse de que la sincronización y coherencia entre estos modelos se mantenga.

En entornos donde las operaciones de lectura y escritura no difieren significativamente en términos de escala o complejidad, implementar CQRS podría ser innecesario y agregar una sobrecarga adicional sin beneficios claros. Por lo tanto, CQRS se adapta mejor a situaciones donde hay una disparidad clara entre cómo se deben manejar las querys y los commands y donde los beneficios de rendimiento y escalabilidad superan los costes adicionales de mantener modelos separados.

Conclusión

En resumen, CQRS es una poderosa metodología que, cuando se aplica en el contexto adecuado, puede llevar a un software altamente escalable, mantenible y claro. Sin embargo, los desarrolladores deben sopesar cuidadosamente los beneficios frente a la complejidad adicional para determinar si es la mejor elección para su aplicación. Y sobre todo deben elegir a qué nivel implementarlo, pues como hemos visto, podemos aportar soluciones con CQRS relativamente sencillas o impliquen crear sistemas completamente independientes y añadir capas de complejidad al sistema, un añadir una capa de complejidad SIEMPRE debe ir de la mano de una buena justificación.