Command y Query Handlers en Go
Capítulo 16: Command y Query Handlers en Go
Los handlers son el corazón de CQRS: reciben comandos o queries y coordinan las operaciones necesarias para ejecutarlos. En Go, implementamos handlers como structs con un método Handle que recibe el comando/query y retorna el resultado.
Anatomía de un Handler
Un handler típico tiene tres responsabilidades:
- Recibir el comando/query con los datos de entrada
- Ejecutar la lógica de negocio (via dominio, repositorios, servicios)
- Retornar el resultado o error
Los handlers reciben sus dependencias por inyección de constructor, lo que facilita testing con mocks.
Command: Crear Pedido
Este handler crea un nuevo pedido y publica los eventos generados.
El patrón de iterar PullEvents() y publicar cada evento es comun: el dominio genera eventos, el handler los persiste y publica.
// internal/command/orders/create_order.go
package orders
import (
"context"
"orderflow/internal/domain/order"
)
type CreateOrderCommand struct {
OrderID string
CustomerID string
}
func (c CreateOrderCommand) CommandName() string { return "CreateOrder" }
type CreateOrderHandler struct {
repo order.Repository
eventBus EventPublisher
}
func NewCreateOrderHandler(repo order.Repository, bus EventPublisher) *CreateOrderHandler {
return &CreateOrderHandler{repo: repo, eventBus: bus}
}
func (h *CreateOrderHandler) Handle(ctx context.Context, cmd CreateOrderCommand) error {
newOrder := order.NewOrder(cmd.OrderID, cmd.CustomerID)
if err := h.repo.Save(ctx, newOrder); err != nil {
return err
}
for _, event := range newOrder.PullEvents() {
if err := h.eventBus.Publish(ctx, event); err != nil {
return err
}
}
return nil
}
Command: Agregar Item
Este handler modifica un pedido existente: lo carga, ejecuta la operación de dominio y persiste los cambios.
El patrón load-modify-save es fundamental en CQRS:
- Cargar el aggregate desde el repositorio
- Invocar métodos de dominio que modifican estado y generan eventos
- Guardar el aggregate actualizado
- Publicar los eventos generados
// internal/command/orders/add_item.go
package orders
import (
"context"
"errors"
"orderflow/internal/domain/order"
)
type AddItemCommand struct {
OrderID string
ProductID string
Name string
Price float64
Quantity int
}
func (c AddItemCommand) CommandName() string { return "AddItem" }
type AddItemHandler struct {
repo order.Repository
eventBus EventPublisher
}
func NewAddItemHandler(repo order.Repository, bus EventPublisher) *AddItemHandler {
return &AddItemHandler{repo: repo, eventBus: bus}
}
func (h *AddItemHandler) Handle(ctx context.Context, cmd AddItemCommand) error {
o, err := h.repo.FindByID(ctx, cmd.OrderID)
if err != nil {
return err
}
if o == nil {
return errors.New("order not found")
}
item := order.Item{
ProductID: cmd.ProductID,
Name: cmd.Name,
Price: cmd.Price,
Quantity: cmd.Quantity,
}
o.AddItem(item)
if err := h.repo.Save(ctx, o); err != nil {
return err
}
for _, event := range o.PullEvents() {
h.eventBus.Publish(ctx, event)
}
return nil
}
Query: Obtener Pedido
Los Query Handlers solo leen datos, nunca modifican estado. Este handler implementa el patrón Cache-Aside: primero intenta leer del caché, si falla, lee del repositorio y actualiza el caché.
CacheRepository es una abstracción que esconde los detalles de Redis, manteniendo el handler desacoplado de la tecnología de caché.
// internal/query/orders/get_order.go
package orders
import (
"context"
"orderflow/internal/domain/order"
)
type GetOrderQuery struct {
OrderID string
}
func (q GetOrderQuery) QueryName() string { return "GetOrder" }
type GetOrderHandler struct {
readRepo order.ReadRepository
cache CacheRepository
}
func NewGetOrderHandler(repo order.ReadRepository, cache CacheRepository) *GetOrderHandler {
return &GetOrderHandler{readRepo: repo, cache: cache}
}
func (h *GetOrderHandler) Handle(ctx context.Context, q GetOrderQuery) (*order.OrderReadModel, error) {
// Intentar desde caché
cached, err := h.cache.Get(ctx, q.OrderID)
if err == nil && cached != nil {
return cached, nil
}
// Buscar en read store
result, err := h.readRepo.FindByID(ctx, q.OrderID)
if err != nil {
return nil, err
}
// Guardar en caché
h.cache.Set(ctx, q.OrderID, result)
return result, nil
}
Query: Listar Pedidos por Cliente
Este handler devuelve una lista paginada de pedidos. La paginación se implementa con slice de Go, aunque en producción debería hacerse a nivel de base de datos.
El resultado incluye metadata de paginación (total, página actual) para que el cliente pueda navegar entre páginas.
// internal/query/orders/list_orders.go
package orders
import (
"context"
"orderflow/internal/domain/order"
)
type ListOrdersQuery struct {
CustomerID string
Page int
Limit int
}
func (q ListOrdersQuery) QueryName() string { return "ListOrders" }
type ListOrdersResult struct {
Orders []order.OrderSummary
Total int
Page int
}
type ListOrdersHandler struct {
readRepo order.ReadRepository
}
func NewListOrdersHandler(repo order.ReadRepository) *ListOrdersHandler {
return &ListOrdersHandler{readRepo: repo}
}
func (h *ListOrdersHandler) Handle(ctx context.Context, q ListOrdersQuery) (*ListOrdersResult, error) {
orders, err := h.readRepo.FindByCustomer(ctx, q.CustomerID)
if err != nil {
return nil, err
}
start := q.Page * q.Limit
end := start + q.Limit
if end > len(orders) {
end = len(orders)
}
return &ListOrdersResult{
Orders: orders[start:end],
Total: len(orders),
Page: q.Page,
}, nil
}
Registro de Handlers
En el bootstrap de la aplicación, registramos cada handler en el mediador con su nombre correspondiente. Esto permite que el mediador enrute comandos/queries al handler correcto.
El uso de alias de import (queryOrders) evita colisiones cuando dos paquetes tienen el mismo nombre.
// cmd/api/main.go
package main
import (
"orderflow/internal/command/orders"
queryOrders "orderflow/internal/query/orders"
"orderflow/pkg/mediator"
)
func setupHandlers(m *mediator.Mediator, deps Dependencies) {
// Command handlers
m.Register("CreateOrderCommand", orders.NewCreateOrderHandler(deps.OrderRepo, deps.EventBus))
m.Register("AddItemCommand", orders.NewAddItemHandler(deps.OrderRepo, deps.EventBus))
// Query handlers
m.Register("GetOrderQuery", queryOrders.NewGetOrderHandler(deps.ReadRepo, deps.Cache))
m.Register("ListOrdersQuery", queryOrders.NewListOrdersHandler(deps.ReadRepo))
}
HTTP Handler
El HTTP Handler actua como adaptador entre el mundo HTTP y CQRS. Traduce requests HTTP en comandos/queries y responses del mediador en respuestas HTTP.
json.NewDecoder parsea el body JSON directamente desde el stream, más eficiente que leer todo el body primero.
// internal/infrastructure/http/orders.go
package http
import (
"encoding/json"
"net/http"
"orderflow/internal/command/orders"
"orderflow/pkg/mediator"
)
type OrdersHandler struct {
mediator *mediator.Mediator
}
func (h *OrdersHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
var req struct {
CustomerID string `json:"customer_id"`
}
json.NewDecoder(r.Body).Decode(&req)
cmd := orders.CreateOrderCommand{
OrderID: generateID(),
CustomerID: req.CustomerID,
}
if _, err := h.mediator.Send(r.Context(), cmd); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{"order_id": cmd.OrderID})
}
Resumen
Los handlers en Go:
- Son simples y enfocados en una sola responsabilidad
- Coordinan dominio, persistencia y eventos
- Queries usan caché para optimizar lecturas
- Se registran en un mediador central
Glosario
Handler
Definición: Componente que recibe un comando o query y ejecuta la lógica necesaria para procesarlo, coordinando dominio, repositorios y servicios.
Por qué es importante: Encapsula la orquestación de operaciones, manteniendo la lógica de negocio en el dominio y la coordinación en un lugar centralizado.
Ejemplo práctico: CreateOrderHandler recibe CreateOrderCommand, crea el Order en el dominio, lo persiste y publica los eventos generados.
EventPublisher
Definición: Interfaz que abstrae la publicación de eventos de dominio a un sistema de mensajería (RabbitMQ, Kafka, etc.).
Por qué es importante: Desacopla los handlers del sistema de mensajería específico, permitiendo cambiar de tecnología sin modificar handlers.
Ejemplo práctico: El handler llama eventBus.Publish(event) sin saber si va a RabbitMQ, Kafka o una cola en memoria para tests.
Load-Modify-Save
Definición: Patrón donde se carga una entidad, se modifica via métodos de dominio, y se guarda el estado actualizado.
Por qué es importante: Garantiza que toda modificación pase por el dominio, donde viven las reglas de negocio y la generación de eventos.
Ejemplo práctico: Cargar Order, llamar order.AddItem(), guardar Order. El método AddItem valida, actualiza el total y genera el evento ItemAdded.
CacheRepository
Definición: Abstracción que define operaciones de caché (Get, Set, Delete) sin exponer detalles de implementación como Redis.
Por qué es importante: Los handlers pueden usar caché sin depender de una tecnología específica. Facilita testing con implementaciones en memoria.
Ejemplo práctico: El handler llama cache.Get(orderId) que podría estar implementado con Redis, Memcached, o un map en memoria para tests.
Paginación
Definición: Técnica para dividir resultados grandes en páginas más pequeñas, retornando solo un subconjunto de datos por request.
Por qué es importante: Evita sobrecargar memoria y red al consultar colecciones grandes. Mejora tiempos de respuesta y experiencia de usuario.
Ejemplo práctico: Obtener pedidos con page=2, limit=20 retorna los pedidos 21-40, junto con el total para saber cuántas páginas hay.
Inyección de Dependencias
Definición: Patrón donde un componente recibe sus dependencias (repositorios, servicios) desde afuera en lugar de crearlas internamente.
Por qué es importante: Facilita testing (inyectar mocks) y desacopla componentes. El handler no necesita saber cómo crear un repositorio.
Ejemplo práctico: NewCreateOrderHandler(repo, eventBus) recibe las dependencias. En producción se pasan implementaciones reales, en tests se pasan mocks.
HTTP Handler / Adaptador
Definición: Componente que traduce requests HTTP en comandos/queries CQRS y traduce las respuestas de vuelta a HTTP.
Por qué es importante: Separa el protocolo de transporte (HTTP) de la lógica de negocio (CQRS). Permite exponer la misma lógica via gRPC, CLI, etc.
Ejemplo práctico: POST /orders se traduce a CreateOrderCommand, se envía al mediador, y la respuesta se serializa como JSON con status 201.
Import Alias
Definición: Renombrar un paquete al importarlo para evitar colisiones de nombres o mejorar legibilidad.
Por qué es importante: Cuando dos paquetes tienen el mismo nombre (ej: orders para commands y queries), los alias permiten distinguirlos.
Ejemplo práctico: import queryOrders "orderflow/internal/query/orders" permite usar queryOrders.GetOrderHandler sin confundir con el paquete de commands.