← Volver al listado de tecnologías

Command y Query Handlers en Go

Por: SiempreListo
cqrsgohandlerscommandsqueries

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:

  1. Recibir el comando/query con los datos de entrada
  2. Ejecutar la lógica de negocio (via dominio, repositorios, servicios)
  3. 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:

  1. Cargar el aggregate desde el repositorio
  2. Invocar métodos de dominio que modifican estado y generan eventos
  3. Guardar el aggregate actualizado
  4. 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:

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.


← Capítulo 15: Go Arquitectura | Capítulo 17: Python CQRS →