
CQRS con ejemplos en TypeScript
En este artículo vamos a explorar el patrón CQRS (Command Query Responsibility Segregation).
Su objetivo es separar las operaciones de lectura (Queries) de las operaciones de escritura (Commands) para obtener sistemas más mantenibles, escalables y fáciles de optimizar.
¿Qué es CQRS?
El patrón CQRS nos dice que:
- Las Queries solo leen datos (no modifican el estado del sistema).
- Los Commands solo modifican datos (pero no devuelven resultados complejos, a lo sumo un identificador o confirmación).
Esto evita mezclar responsabilidades en una misma capa y facilita escalar cada parte de forma independiente.
Ventajas de utilizar CQRS
- ✅ Separación clara de responsabilidades: Lectura y escritura evolucionan sin afectarse mutuamente.
- ✅ Escalabilidad independiente: Podés escalar el lado de lecturas (normalmente más demandado) sin necesidad de escalar escrituras.
- ✅ Optimización por caso de uso: La base de datos de lectura puede estar optimizada para consultas rápidas (ej. MongoDB, Redis), mientras que la de escritura puede garantizar consistencia fuerte (ej. PostgreSQL).
- ✅ Facilita Event Sourcing: CQRS se integra muy bien con patrones de event sourcing, donde cada cambio en el sistema se guarda como un evento.
- ✅ Mantenibilidad: El código se vuelve más fácil de entender y probar.
Desventajas de CQRS
- ❌ Mayor complejidad inicial: No siempre se justifica para sistemas pequeños.
- ❌ Consistencia eventual: En escenarios distribuidos, puede que las lecturas no reflejen inmediatamente las escrituras.
- ❌ Curva de aprendizaje: Introduce más capas y patrones (commands, queries, handlers, buses de mensajes).
Casos de uso ideales
- Sistemas con altísima carga de lectura (ej. aplicaciones de e-commerce con millones de productos).
- Aplicaciones que requieren auditoría y trazabilidad (ej. banca, fintech, seguros).
- Arquitecturas de microservicios donde cada módulo puede manejar lecturas y escrituras por separado.
- Escenarios donde la performance de consultas es crítica y necesitamos optimizar la lectura sin comprometer la escritura.
Ejemplo básico en TypeScript
Imaginemos un microservicio de usuarios.
1. Definimos nuestros modelos
// domain/User.ts
export interface User {
id: string;
name: string;
email: string;
}
2. Creamos los Commands
Los commands representan acciones que cambian el estado:
// commands/CreateUserCommand.ts
export class CreateUserCommand {
constructor(
public readonly name: string,
public readonly email: string
) {}
}
El handler de este comando sería el encargado de ejecutarlo:
// commands/handlers/CreateUserHandler.ts
import { CreateUserCommand } from "../CreateUserCommand";
import { User } from "../../domain/User";
export class CreateUserHandler {
private users: User[] = [];
execute(command: CreateUserCommand): User {
const newUser: User = {
id: crypto.randomUUID(),
name: command.name,
email: command.email,
};
this.users.push(newUser);
return newUser;
}
}
3. Creamos las Queries
Las queries representan consultas que solo leen datos:
// queries/GetUserByEmailQuery.ts
export class GetUserByEmailQuery {
constructor(public readonly email: string) {}
}
Handler de la query:
// queries/handlers/GetUserByEmailHandler.ts
import { GetUserByEmailQuery } from "../GetUserByEmailQuery";
import { User } from "../../domain/User";
export class GetUserByEmailHandler {
constructor(private readonly users: User[]) {}
execute(query: GetUserByEmailQuery): User | undefined {
return this.users.find(u => u.email === query.email);
}
}
Ejemplo de uso combinado
import { CreateUserCommand } from "./commands/CreateUserCommand";
import { CreateUserHandler } from "./commands/handlers/CreateUserHandler";
import { GetUserByEmailQuery } from "./queries/GetUserByEmailQuery";
import { GetUserByEmailHandler } from "./queries/handlers/GetUserByEmailHandler";
// Inicializamos el handler de escritura
const createUserHandler = new CreateUserHandler();
// Creamos un usuario
const command = new CreateUserCommand("Max Rossi", "max@example.com");
const newUser = createUserHandler.execute(command);
console.log("Usuario creado:", newUser);
// Ahora inicializamos el handler de lectura con la misma fuente de datos
const getUserHandler = new GetUserByEmailHandler([newUser]);
// Ejecutamos una query
const query = new GetUserByEmailQuery("max@example.com");
const userFound = getUserHandler.execute(query);
console.log("Usuario encontrado:", userFound);
Conclusión
Con CQRS separamos las responsabilidades de lectura y escritura.
Esto nos ayuda a construir sistemas más limpios, fáciles de probar y que escalan de manera independiente.
Este ejemplo fue sencillo, pero CQRS brilla en arquitecturas de microservicios o sistemas distribuidos donde la complejidad crece rápidamente.
👉 En próximos artículos podríamos ver cómo combinar CQRS + Event Sourcing, y cómo usar buses de mensajes (ej. Kafka o RabbitMQ) para manejar la comunicación entre comandos y queries.