CQRS y Event Sourcing en TypeScript: 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

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\n

Hola, 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\n

Si desarrollas fintech, marketplaces, CRM o cualquier sistema donde la historia y la auditoría sean importantes, este artículo es para ti.

\n\n

Qué es CQRS en palabras simples

\n\n

Imagina 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\n

En 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
\n\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\n

Ejemplo en TypeScript: CQRS simple sin Event Sourcing

\n\n

Supongamos 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\n

Ahora 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\n

Y 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                

Blogs

Book Recommendations