CQRS y Event Sourcing en PHP: Arquitectura para sistemas complejos

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Cuando el CRUD tradicional ya no funciona: Introducción a CQRS y Event Sourcing

\n\n

¿Alguna vez te ha pasado que tu aplicación PHP crecía, crecía... y un buen día el típico UserController se convertía en un monstruo de 2000 líneas? ¿Te suena familiar? Los errores se multiplican, el rendimiento cae y añadir una nueva funcionalidad provoca un tic nervioso en todo el equipo. Felicidades, has llegado al punto en el que necesitas implementar patrones arquitectónicos serios.

\n\n

Hoy hablaremos de dos herramientas muy potentes: CQRS (Command Query Responsibility Segregation) y Event Sourcing. No te asustes con los términos complejos. En realidad, son formas elegantes de decir: "Dejemos de mezclar la lectura y escritura de datos" y "Guardemos no el estado actual, sino el historial de cambios".

\n\n

Este artículo está dirigido a desarrolladores PHP que ya dominan los fundamentos de la POO y quieren llevar sus proyectos al siguiente nivel. Aprenderás a aplicar CQRS y Event Sourcing en la práctica, verás código real y entenderás cuándo estos patrones son realmente necesarios y cuándo es mejor evitarlos.

\n\n

¿Qué es CQRS? Separando comandos y consultas

\n\n

Imagina que tu código es un restaurante. Normalmente, el camarero (controlador) toma el pedido (solicitud de escritura), corre a la cocina (base de datos), prepara el plato (procesa los datos) y lo sirve al instante (devuelve la respuesta). Todo funciona bien cuando hay 10 personas en el restaurante. Pero cuando llegan 1000 comensales, la cocina comienza a colapsar.

\n\n

CQRS (Command Query Responsibility Segregation) propone separar responsabilidades: unos cocineros (comandos) solo cocinan, otros camareros (consultas) solo sirven. Técnicamente, esto significa que usas modelos diferentes para leer y escribir datos.

\n\n

Implementación básica de CQRS en PHP

\n\n

Veamos cómo se ve esto en código. Empezaremos con un ejemplo sencillo sin frameworks para entender la esencia.

\n\n
// Comando: un DTO con datos para cambiar el estado\nclass CreateUserCommand {\n    public function __construct(\n        public readonly string $email,\n        public readonly string $name\n    ) {}\n}\n\n// Manejador del comando: aquí ocurre la lógica de negocio\nclass CreateUserHandler {\n    public function __construct(\n        private UserRepository $repository,\n        private EventDispatcher $dispatcher\n    ) {}\n\n    public function handle(CreateUserCommand $command): void {\n        $user = new User($command->email, $command->name);\n        $this->repository->save($user);\n        $this->dispatcher->dispatch(new UserCreatedEvent($user->getId()));\n    }\n}\n\n// Consulta: un DTO para obtener datos\nclass GetUserQuery {\n    public function __construct(\n        public readonly int $userId\n    ) {}\n}\n\n// Manejador de consulta: solo lectura, sin cambios\nclass GetUserHandler {\n    public function __construct(\n        private UserReadRepository $readRepository\n    ) {}\n\n    public function handle(GetUserQuery $query): ?UserDTO {\n        return $this->readRepository->findById($query->userId);\n    }\n}
\n\n

Observa que tenemos dos repositorios diferentes. UserRepository se encarga de la escritura (puede usar lógica de negocio compleja, validación), mientras que UserReadRepository se encarga de la lectura (puede ser una consulta SQL simple o incluso una tabla desnormalizada).

\n\n

Bus de comandos (Command Bus)

\n\n

En proyectos reales, los comandos y consultas se envían a través de un bus: un despachador central que encuentra automáticamente el manejador adecuado.

\n\n
// Implementación simple de un Command Bus\nclass SimpleCommandBus {\n    public function __construct(\n        private array $handlers = []\n    ) {}\n\n    public function registerHandler(string $commandClass, object $handler): void {\n        $this->handlers[$commandClass] = $handler;\n    }\n\n    public function dispatch(object $command): void {\n        $handler = $this->handlers[$co                

Blogs

Book Recommendations