CQRS: Command Query Responsibility Segregation
CQRS separa el modelo de datos en dos lados: el lado de Comando (escritura) optimizado para validar y persistir cambios de estado, y el lado de Consulta (lectura) optimizado para devolver datos desnormalizados y rápidos de leer. Los comandos modifican el estado y publican eventos; las consultas leen vistas materializadas actualizadas por esos eventos.
En una línea CommandHandler -> eventStore.save(evento) -> EventListener -> readModelRepository.update(vistaConsulta)
// Lado de escritura: comando con validación de invariantes
public record AsignarTarea(long tareaId, String responsable) {}
@Service
class ComandosTarea {
@Transactional
public void manejar(AsignarTarea cmd) {
var tarea = repo.findById(cmd.tareaId()).orElseThrow();
tarea.asignarA(cmd.responsable()); // reglas del dominio
eventos.publishEvent(new TareaAsignada(cmd.tareaId(), cmd.responsable()));
}
}
// Lado de lectura: tabla desnormalizada pensada para el panel
public record ResumenResponsable(String responsable, int pendientes, int completadasSemana) {}
@Component
class ProyeccionPanel {
private final JdbcClient jdbc;
ProyeccionPanel(JdbcClient jdbc) { this.jdbc = jdbc; }
@TransactionalEventListener
void on(TareaAsignada e) {
jdbc.sql("""
INSERT INTO panel_responsable (responsable, pendientes) VALUES (:r, 1)
ON CONFLICT (responsable) DO UPDATE SET pendientes = panel_responsable.pendientes + 1
""").param("r", e.responsable()).update();
}
public List<ResumenResponsable> panel() { // consulta trivial y rápida
return jdbc.sql("SELECT * FROM panel_responsable ORDER BY pendientes DESC")
.query(ResumenResponsable.class).list();
}
}
Cómo funciona por dentro
CQRS separa las operaciones que cambian el estado (comandos) de las que lo leen (consultas), usando modelos distintos para cada lado. El modelo de escritura se centra en validar reglas de negocio e invariantes; el de lectura, en servir consultas rápidas con la forma exacta que necesita cada pantalla o API, a menudo desnormalizado.
No implica obligatoriamente dos bases de datos. El nivel más sencillo es el mismo almacenamiento con dos modelos en el código (entidades ricas para escribir, proyecciones o vistas SQL para leer). El nivel avanzado usa almacenes separados (PostgreSQL para escribir, Elasticsearch o una tabla desnormalizada para leer) sincronizados mediante eventos, con consistencia eventual.
Tiene sentido cuando lecturas y escrituras son muy asimétricas en volumen o forma: un panel que agrega datos de muchas tablas, búsquedas de texto completo o lecturas que se cuentan por miles frente a pocas escrituras.
Se conecta con Event SourcingVistas MaterializadasEscalabilidad Asimétrica