Cuando el CRUD habitual ya no es suficiente: introducción a CQRS y Event Sourcing
\nImagine que está construyendo un sistema de reserva de boletos. Todo es sencillo mientras hay diez usuarios. Pero cuando se convierten en mil, y luego en un millón, comienzan los problemas: las bases de datos se bloquean, las consultas entran en conflicto y el historial de cambios es imposible de recuperar. ¿Le suena familiar?
\nEn el mundo del desarrollo en Go, donde el rendimiento y el paralelismo lo son todo, dos patrones arquitectónicos ayudan a resolver estos problemas: CQRS (Command Query Responsibility Segregation) y Event Sourcing. Hoy los analizaremos en la práctica, con código, ejemplos y escenarios de uso reales.
\n\n¿Qué es CQRS en términos sencillos?
\nCQRS es la separación de responsabilidades entre comandos (escritura) y consultas (lectura). En el CRUD clásico, un único modelo de datos se encarga tanto de la creación como de la lectura. En CQRS, creamos dos modelos separados:
\n- \n
- Modelo de comando — se encarga de cambiar el estado (crear, actualizar, eliminar) \n
- Modelo de consulta — se encarga solo de la lectura de datos \n
Esto permite optimizar cada lado de forma independiente: la escritura puede ser estrictamente validada, y la lectura puede ser desnormalizada y rápida. En Go, esto es especialmente eficaz gracias a las gorutinas y los canales.
\n\nEjemplo sencillo de CQRS en Go
\nVamos a crear un sistema básico para la gestión de pedidos. Empezamos con los comandos:
\n\ntype CreateOrderCommand struct {\n OrderID string\n UserID string\n ProductID string\n Amount float64\n}\n\ntype CommandHandler struct {\n repo *OrderRepository\n}\n\nfunc (h *CommandHandler) HandleCreateOrder(ctx context.Context, cmd CreateOrderCommand) error {\n order := Order{\n ID: cmd.OrderID,\n UserID: cmd.UserID,\n ProductID: cmd.ProductID,\n Amount: cmd.Amount,\n Status: "pending",\n CreatedAt: time.Now(),\n }\n return h.repo.Save(ctx, order)\n}\n\nAhora creamos un modelo separado para la lectura:
\n\ntype OrderQuery struct {\n repo *ReadRepository\n}\n\ntype OrderView struct {\n OrderID string\n UserName string\n ProductName string\n Total float64\n Status string\n}\n\nfunc (q *OrderQuery) GetOrderSummary(ctx context.Context, orderID string) (*OrderView, error) {\n return q.repo.GetOrderView(ctx, orderID)\n}\n\nObserve que en OrderView ya hay datos desnormalizados (nombre de usuario, nombre del producto) que se recopilan de diferentes fuentes. Esto acelera la lectura decenas de veces.
Event Sourcing: registramos no el estado, sino los eventos
\nEvent Sourcing cambia el enfoque del almacenamiento de datos: en lugar de almacenar el estado actual de un objeto, almacenamos la secuencia de eventos que llevaron a ese estado. Esto otorga un superpoder: el historial completo de cambios y la capacidad de «rebobinar» el estado.
\n\nImplementación de Event Sourcing en Go
\nEmpezamos con los tipos básicos de eventos:
\n\ntype Event struct {\n ID string\n AggregateID string\n Type string\n Data []byte\n Timestamp time.Time\n Version int\n}\n\ntype EventStore interface {\n Save(ctx context.Context, events []Event) error\n Load(ctx context.Context, aggregateID string) ([]Event, error)\n}\n\nAhora implementamos el agregado — el objeto que restaura su estado a partir de los eventos:
\n\ntype OrderAggregate struct {\n ID string\n Status string\n Items []OrderItem\n Version int\n}\n\nfunc (a *OrderAggregate) Apply(event Event) {\n switch event.Type {\n case "OrderCreated":\n var data OrderCreatedData\n json.Unmarshal(event.Data, &data)\n a.ID = data.OrderID\n a.Status = "created"\n case "OrderConfirmed":\n a.Status = "confirmed"\n case "OrderShipped":\n a.Stat