Por qué las aplicaciones CRUD comunes dejan de funcionar cuando el negocio se vuelve complejo
\n\n¿Alguna vez te has enfrentado a una situación en la que una tarea simple — "mostrar el historial de cambios de un pedido" — se convierte en un infierno? Revisas la base de datos, miras los registros, intentas reconstruir quién y cuándo cambió el estado. Y luego llega una solicitud: "Haz una auditoría de todas las acciones del usuario en los últimos tres años". Y te das cuenta de que la arquitectura actual no lo soportará.
\n\nHola, me llamo PythonLib, y hoy analizaremos dos patrones poderosos que salvan en estas situaciones: CQRS (Command Query Responsibility Segregation) y Event Sourcing. Escribiremos código en TypeScript que puedes usar inmediatamente en un proyecto real.
\n\nSi desarrollas fintech, marketplaces, CRM o cualquier sistema donde la historia y la auditoría sean importantes, este artículo es para ti.
\n\nQué es CQRS en palabras simples
\n\nImagina una biblioteca. Normalmente, un solo bibliotecario entrega libros, los recibe y lleva el registro. Pero cuando hay muchos visitantes, se forma una cola. CQRS propone dividir las responsabilidades: un bibliotecario solo entrega (comandos), el segundo solo recibe y registra (consultas).
\n\nEn programación, esto significa separar los modelos de lectura y escritura. En lugar de un repositorio monolítico, creamos:
\n\n- \n
- Command Model — para modificar datos (Create, Update, Delete) \n
- Query Model — para leer datos (Read) \n
¿Parece una complicación? Sí, al principio. Pero cuando el sistema crece, obtienes flexibilidad: puedes optimizar la lectura por separado de la escritura, usar diferentes bases de datos, cachés.
\n\nEjemplo en TypeScript: CQRS simple sin Event Sourcing
\n\nSupongamos que tenemos un sistema de gestión de pedidos. Primero, definimos las entidades:
\n\n// domain/order.ts\nexport interface Order {\n id: string;\n userId: string;\n items: OrderItem[];\n status: 'pending' | 'confirmed' | 'shipped' | 'cancelled';\n total: number;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface OrderItem {\n productId: string;\n quantity: number;\n price: number;\n}\n\n\nAhora creamos un comando y una consulta:
\n\n// application/commands/create-order.command.ts\nexport class CreateOrderCommand {\n constructor(\n public readonly userId: string,\n public readonly items: OrderItem[]\n ) {}\n}\n\n// application/commands/create-order.handler.ts\nimport { CommandHandler, ICommandHandler } from '@nestjs/cqrs';\nimport { OrderRepository } from '../repositories/order.repository';\n\n@CommandHandler(CreateOrderCommand)\nexport class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {\n constructor(private readonly orderRepo: OrderRepository) {}\n\n async execute(command: CreateOrderCommand): Promise<string> {\n const order: Order = {\n id: uuid(),\n userId: command.userId,\n items: command.items,\n status: 'pending',\n total: command.items.reduce((sum, item) => sum + item.price * item.quantity, 0),\n createdAt: new Date(),\n updatedAt: new Date()\n };\n await this.orderRepo.save(order);\n return order.id;\n }\n}\n\n\nY para la lectura, un manejador separado:
\n\n// application/queries/get-order.query.ts\nexport class GetOrderQuery {\n constructor(public readonly orderId: string) {}\n}\n\n// application/queries/get-order.handler.ts\nimport { QueryHandler, IQueryHandler } from '@nestjs/cqrs';\nimport { OrderReadRepository } from '../repositories/order-read.repository';\n\n@QueryHandler(GetOrderQuery)\nexport class GetOrderHandler implements IQueryHandler<GetOrderQuery> {\n constructor(private readonly readRepo: OrderReadRepository) {}\n\n async execute(query: GetOrderQuery): Promise<Order | null> {\n return this.readRepo.findById(query.orderId);\n }\n}\n