← Volver al listado de tecnologías

CQRS en Go - Arquitectura

Por: SiempreListo
cqrsgoarquitecturaclean-architecture

Capítulo 15: CQRS en Go - Arquitectura

Go es un lenguaje compilado creado por Google que destaca por su simplicidad, rendimiento y excelente soporte para concurrencia. Estas características lo hacen ideal para sistemas CQRS que manejan alto volumen de operaciones.

Por qué Go para CQRS

Go ofrece ventajas específicas para CQRS:

Estructura del Proyecto

La estructura sigue principios de Clean Architecture: el dominio está en el centro, libre de dependencias externas. Las capas externas (infrastructure) dependen de las internas (domain), nunca al revés.

orderflow-go/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── domain/
│   │   ├── order/
│   │   │   ├── order.go
│   │   │   └── events.go
│   │   └── events.go
│   ├── command/
│   │   ├── bus.go
│   │   ├── handler.go
│   │   └── orders/
│   │       ├── create_order.go
│   │       └── add_item.go
│   ├── query/
│   │   ├── bus.go
│   │   ├── handler.go
│   │   └── orders/
│   │       ├── get_order.go
│   │       └── list_orders.go
│   ├── projection/
│   │   ├── manager.go
│   │   └── order_summary.go
│   └── infrastructure/
│       ├── postgres/
│       ├── elasticsearch/
│       └── redis/
├── pkg/
│   └── mediator/
│       └── mediator.go
└── go.mod

Interfaces Base

En Go, las interfaces se definen implícitamente: cualquier tipo que implemente los métodos requeridos satisface la interfaz automáticamente (duck typing).

Generics (introducidos en Go 1.18) permiten crear handlers tipados que trabajan con tipos específicos de comandos.

// internal/command/handler.go
package command

type Command interface {
    CommandName() string
}

type Handler[C Command] interface {
    Handle(ctx context.Context, cmd C) error
}

type Bus interface {
    Dispatch(ctx context.Context, cmd Command) error
    Register(cmdName string, handler any)
}
// internal/query/handler.go
package query

type Query interface {
    QueryName() string
}

type Handler[Q Query, R any] interface {
    Handle(ctx context.Context, q Q) (R, error)
}

type Bus interface {
    Ask(ctx context.Context, q Query) (any, error)
    Register(queryName string, handler any)
}

Modelo de Dominio

El Aggregate Order encapsula toda la lógica de negocio. Los métodos modifican el estado interno y registran eventos que representan los cambios ocurridos.

PullEvents() extrae los eventos pendientes de publicar, limpiando la lista interna. Este patrón asegura que cada evento se publique solo una vez.

// internal/domain/order/order.go
package order

import "time"

type Order struct {
    ID         string
    CustomerID string
    Items      []Item
    Status     Status
    Total      float64
    CreatedAt  time.Time
    events     []DomainEvent
}

type Item struct {
    ProductID string
    Name      string
    Price     float64
    Quantity  int
}

type Status string

const (
    StatusPending   Status = "pending"
    StatusConfirmed Status = "confirmed"
    StatusShipped   Status = "shipped"
)

func NewOrder(id, customerID string) *Order {
    o := &Order{
        ID:         id,
        CustomerID: customerID,
        Status:     StatusPending,
        CreatedAt:  time.Now(),
    }
    o.record(OrderCreated{OrderID: id, CustomerID: customerID})
    return o
}

func (o *Order) AddItem(item Item) {
    o.Items = append(o.Items, item)
    o.Total += item.Price * float64(item.Quantity)
    o.record(ItemAdded{OrderID: o.ID, Item: item})
}

func (o *Order) record(event DomainEvent) {
    o.events = append(o.events, event)
}

func (o *Order) PullEvents() []DomainEvent {
    events := o.events
    o.events = nil
    return events
}

Eventos de Dominio

Los eventos son structs inmutables que representan hechos que ocurrieron en el sistema. Cada evento implementa la interfaz DomainEvent con métodos para identificar el tipo y momento del evento.

// internal/domain/order/events.go
package order

import "time"

type DomainEvent interface {
    EventName() string
    OccurredAt() time.Time
}

type OrderCreated struct {
    OrderID    string
    CustomerID string
    occurredAt time.Time
}

func (e OrderCreated) EventName() string    { return "order.created" }
func (e OrderCreated) OccurredAt() time.Time { return e.occurredAt }

type ItemAdded struct {
    OrderID string
    Item    Item
    occurredAt time.Time
}

func (e ItemAdded) EventName() string    { return "order.item_added" }
func (e ItemAdded) OccurredAt() time.Time { return e.occurredAt }

Mediador (Command/Query Bus)

El Mediator desacopla quién envía comandos/queries de quién los maneja. Usa reflection para invocar dinámicamente el método Handle del handler registrado.

reflect es el paquete de Go para introspección de tipos en tiempo de ejecución. Aunque es poderoso, debe usarse con cuidado por su impacto en rendimiento.

// pkg/mediator/mediator.go
package mediator

import (
    "context"
    "fmt"
    "reflect"
)

type Mediator struct {
    handlers map[string]any
}

func New() *Mediator {
    return &Mediator{handlers: make(map[string]any)}
}

func (m *Mediator) Register(name string, handler any) {
    m.handlers[name] = handler
}

func (m *Mediator) Send(ctx context.Context, request any) (any, error) {
    name := reflect.TypeOf(request).Name()
    handler, ok := m.handlers[name]
    if !ok {
        return nil, fmt.Errorf("handler not found for: %s", name)
    }

    method := reflect.ValueOf(handler).MethodByName("Handle")
    results := method.Call([]reflect.Value{
        reflect.ValueOf(ctx),
        reflect.ValueOf(request),
    })

    if len(results) == 2 && !results[1].IsNil() {
        return results[0].Interface(), results[1].Interface().(error)
    }
    return results[0].Interface(), nil
}

Repositorios

Los repositorios definen contratos (interfaces) para persistencia. El Write Side usa Repository para guardar aggregates completos. El Read Side usa ReadRepository para obtener vistas optimizadas.

Los struct tags como `json:"id"` indican cómo serializar campos a JSON.

// internal/domain/order/repository.go
package order

import "context"

type Repository interface {
    Save(ctx context.Context, order *Order) error
    FindByID(ctx context.Context, id string) (*Order, error)
}

type ReadRepository interface {
    FindByID(ctx context.Context, id string) (*OrderReadModel, error)
    FindByCustomer(ctx context.Context, customerID string) ([]OrderSummary, error)
}

type OrderReadModel struct {
    ID         string    `json:"id"`
    CustomerID string    `json:"customer_id"`
    Items      []Item    `json:"items"`
    Status     string    `json:"status"`
    Total      float64   `json:"total"`
}

type OrderSummary struct {
    ID     string  `json:"id"`
    Status string  `json:"status"`
    Total  float64 `json:"total"`
}

Resumen

La arquitectura Go para CQRS:

Glosario

Go (Golang)

Definición: Lenguaje de programación compilado, tipado estáticamente, diseñado por Google. Enfatiza simplicidad, concurrencia y eficiencia.

Por qué es importante: Compila a binarios nativos sin dependencias, ideal para microservicios y sistemas de alto rendimiento como CQRS.

Ejemplo práctico: Un servicio CQRS en Go puede manejar 100,000 requests/segundo con latencias de milisegundos, compilando a un binario de 10MB sin runtime externo.


Generics

Definición: Característica de Go 1.18+ que permite escribir funciones y tipos parametrizados que funcionan con múltiples tipos de datos.

Por qué es importante: Elimina duplicación de código al crear handlers que funcionan con cualquier tipo de comando o query.

Ejemplo práctico: Handler[C Command] es un handler genérico que puede especializarse como Handler[CreateOrderCommand] o Handler[AddItemCommand].


context.Context

Definición: Tipo estándar de Go que transporta deadlines, cancelación y valores request-scoped a través de la cadena de llamadas.

Por qué es importante: Permite cancelar operaciones largas, propagar timeouts y pasar metadata (como trace IDs) sin modificar firmas de funciones.

Ejemplo práctico: Si un cliente cancela su request, el Context se cancela y todas las operaciones downstream (queries a DB, llamadas HTTP) pueden terminar anticipadamente.


Reflection (reflect)

Definición: Capacidad de inspeccionar y manipular tipos en tiempo de ejecución. El paquete reflect proporciona esta funcionalidad en Go.

Por qué es importante: Permite crear mediadores genéricos que invocan handlers sin conocer sus tipos específicos en tiempo de compilación.

Ejemplo práctico: El Mediator usa reflection para encontrar y llamar el método Handle del handler correcto basándose en el nombre del comando.


Struct Tags

Definición: Anotaciones de metadata en campos de structs, encerradas en backticks. Procesadas por paquetes como encoding/json o ORMs.

Por qué es importante: Permiten personalizar serialización, validación y mapeo a bases de datos sin modificar nombres de campos.

Ejemplo práctico: `json:"customer_id"` hace que el campo CustomerID se serialice como customer_id en JSON (snake_case en lugar de PascalCase).


Clean Architecture

Definición: Patrón arquitectónico donde las dependencias apuntan hacia adentro: capas externas (infraestructura) dependen de internas (dominio), nunca al revés.

Por qué es importante: El dominio permanece puro y testeable, sin depender de frameworks, bases de datos u otras tecnologías específicas.

Ejemplo práctico: Order en el dominio no importa PostgreSQL ni Elasticsearch. Los repositorios en infrastructure implementan las interfaces definidas por el dominio.


internal/ (Paquete privado)

Definición: Directorio especial en Go cuyo contenido solo puede importarse desde el mismo módulo, no desde proyectos externos.

Por qué es importante: Protege código interno de ser usado como API pública, permitiendo refactorizaciones sin romper compatibilidad.

Ejemplo práctico: Otros proyectos pueden importar orderflow/pkg/mediator pero no orderflow/internal/domain/order.


← Capítulo 14: Caché Redis | Capítulo 16: Go Handlers →