La primera vez que implementé CQRS en serio fue porque un solo modelo de datos ya no daba abasto: las escrituras necesitaban integridad transaccional estricta, y las lecturas necesitaban servir miles de consultas por segundo con filtros que el modelo transaccional no estaba diseñado para resolver bien. Intentar que una sola tabla, un solo esquema, cumpliera ambos objetivos nos tenía peleando contra la base de datos en vez de resolviendo el problema del negocio.

CQRS (Command Query Responsibility Segregation) parte de una idea simple: separar el modelo que recibe comandos (escrituras) del modelo que responde consultas (lecturas). Lo que casi nadie te cuenta cuando lo propone en una reunión es el precio real de esa separación.

El patrón, sin diagramas de caja y flecha

El lado de comandos valida reglas de negocio y persiste el cambio como la fuente de verdad. El lado de consultas mantiene una o más "proyecciones" — copias del dato, optimizadas para lectura, que se actualizan de forma asíncrona a partir de lo que ocurrió en el lado de comandos.

// Lado de comandos: valida y persiste la intención
async function handleCreateOrder(cmd: CreateOrderCommand) {
  const order = Order.create(cmd.customerId, cmd.items) // reglas de negocio acá
  await orderRepository.save(order)
  await eventBus.publish(new OrderCreatedEvent(order.id, order.items))
}

// Lado de consultas: se actualiza reaccionando al evento, no al comando
async function onOrderCreated(event: OrderCreatedEvent) {
  await orderSummaryReadModel.upsert({
    orderId: event.orderId,
    itemCount: event.items.length,
    status: 'created',
  })
}

Nada exótico. La complejidad no está en el código, está en lo que pasa entre esas dos funciones.

El precio: consistencia eventual

Entre que se guarda el comando y se actualiza la proyección de lectura, hay una ventana de tiempo —milisegundos, casi siempre— en la que el sistema está en un estado inconsistente: la escritura ya ocurrió, pero si consultas la vista de lectura todavía no la vas a ver reflejada. Eso es consistencia eventual, y no es un defecto de implementación: es una consecuencia directa de separar los modelos.

El error que veo más seguido es tratar esa ventana como si no existiera, hasta que un usuario crea un pedido, actualiza la página inmediatamente, no lo ve, y abre un ticket de soporte convencido de que el sistema perdió su pedido. El sistema no perdió nada. La arquitectura simplemente no le avisó a nadie que había un delay.

Cuándo vale la pena (y cuándo es sobre-ingeniería)

CQRS tiene sentido cuando el patrón de lectura y el de escritura son genuinamente distintos: alta frecuencia de lectura con formas de consulta muy variadas, escrituras que requieren validación de negocio pesada, o necesidad de escalar lecturas y escrituras de forma completamente independiente.

No tiene sentido para un CRUD estándar donde leer y escribir el mismo modelo funciona perfectamente bien. He visto equipos meter CQRS completo — con bus de eventos, proyecciones separadas y almacenamiento distinto para cada lado — en sistemas que hubieran estado igual de bien servidos por una tabla y un índice. La pregunta que me hago antes de proponerlo no es "¿es un patrón válido?", es "¿el problema que tengo justifica la complejidad operacional que estoy a punto de introducir?".

Cómo convivir con la ventana de inconsistencia

Si decides que sí vale la pena, hay formas razonables de que la consistencia eventual no se sienta como un bug:

  • UI optimista: actualiza la interfaz con el resultado esperado apenas se acepta el comando, sin esperar a que la proyección de lectura confirme.
  • Tokens de versión: adjunta al comando la versión del dato que el cliente vio, para que el sistema pueda decidir si el estado leído está desactualizado antes de aceptar un cambio.
  • Proyecciones idempotentes: diseña el consumidor de eventos para que procesar el mismo evento dos veces no rompa nada — los reintentos y las entregas duplicadas son la norma, no la excepción, en sistemas basados en eventos.
  • Comunica el delay cuando importa: si hay una operación donde el usuario necesita ver su cambio reflejado de inmediato, considera leer directo del modelo de escritura para ese caso puntual, en vez de forzar que todo pase por la proyección eventual.

CQRS no resuelve el problema de la consistencia, lo hace explícito. Y en arquitectura, hacer explícito un trade-off que ya existía —pero estaba escondido— casi siempre vale más que fingir que no existe.