Implementación de CQRS en Go: una guía práctica para una arquitectura escalable

Construye CQRS en Go sin ceremonias innecesarias

Índice

CQRS es uno de esos patrones que se sobrevenden, se complican en exceso y, ocasionalmente, se diagnostican erróneamente como la cura para el aburrimiento del CRUD tradicional.

La versión útil es mucho más sencilla: separa el código que cambia el estado del código que lee el estado y permite que cada lado evolucione para su propia función. Martin Fowler describe CQRS como el uso de un modelo diferente para actualizar la información que el utilizado para leerla, advirtiendo al mismo tiempo que, para la mayoría de los sistemas, añade una complejidad riesgosa. Microsoft hace la misma afirmación central en términos más operativos: separa los modelos de lectura y escritura para que cada uno pueda optimizarse de forma independiente.

CQRS en Go: comandos y consultas como rutas separadas a través de un hub de gopher de Go

Si trabajas con Go, esa idea se adapta de manera inusualmente bien al lenguaje. Go es bueno para límites explícitos, interfaces pequeñas, tipos de datos sencillos y paquetes orientados a casos de uso. Eso hace que el CQRS básico en Go sea mucho menos teatral de lo que a menudo parece en las diapositivas de conferencias. No necesitas event sourcing (registro de eventos), Kafka ni tres bases de datos para comenzar. De hecho, tanto las directrices de CQRS de Microsoft como los ejemplos de Go de Three Dots Labs muestran que una implementación simple puede compartir el mismo almacén subyacente, con manejadores de comandos y consultas separados añadidos primero y una infraestructura más elaborada introducida solo cuando el problema realmente lo exige.

Qué significa realmente CQRS

En el fondo, CQRS traza una línea dura entre comandos y consultas. Una consulta lee datos y no debería modificar el estado del sistema. Un comando cambia el estado y no debería devolver datos de dominio como su resultado principal. Three Dots Labs lo expresan en términos prácticos de Go: las consultas devuelven datos y los comandos realizan cambios, siendo los errores un resultado normal de los comandos. Ese es el movimiento básico. Todo lo demás es opcional.

Un malentendido común es que CQRS significa automáticamente bases de datos separadas, proyecciones asíncronas o event sourcing. Eso no es cierto. La guía de patrones de Microsoft trata explícitamente los almacenes de datos separados como la forma más avanzada, no como la predeterminada, y Three Dots Labs muestran una implementación en Go donde las consultas leen de la misma base de datos que las escrituras, porque eso es suficiente para el sistema en cuestión. Si tu artículo solo enseña una cosa claramente, que sea esta: CQRS es principalmente una elección de modelado y estructura de aplicación, no un paquete obligatorio de sistemas distribuidos.

El otro detalle importante es la nomenclatura. Los comandos deben modelar la intención del negocio, no mutaciones de almacenamiento. El ejemplo de Microsoft contrasta “Reservar habitación de hotel” con “Establecer EstadoDeReserva a Reservado”, y Three Dots Labs recomiendan nombres cercanos a la forma en que hablan los expertos del dominio, como “ProgramarEntrenamiento” o “CancelarEntrenamiento” en lugar de verbos genéricos como “Crear” y “Eliminar”. En Go, esa disciplina de nomenclatura tiene sus recompensas porque los nombres de los comandos a menudo se convierten en nombres de tipos, nombres de manejadores y límites de paquetes.

Por qué los equipos buscan este patrón

CQRS se vuelve atractivo cuando un solo modelo CRUD comienza a desempeñar demasiados trabajos de manera deficiente. Las directrices de Microsoft enumeran los puntos de presión habituales: las representaciones de lectura y escritura de los mismos datos divergen, las actualizaciones concurrentes crean contención de bloqueos, el rendimiento de lectura sufre bajo la complejidad de las consultas y las entidades compartidas convierten las reglas de seguridad en un enredo. En otras palabras, el problema no es que CRUD sea moralmente incorrecto. El problema es que un solo modelo se ve obligado a satisfacer preocupaciones incompatibles al mismo tiempo.

Esto es especialmente común en productos técnicos. Las escrituras suelen preocuparse por la validación, los invariantes, las transacciones y las reglas de negocio. Las lecturas suelen preocuparse por filtros, uniones, agregación, caché, ordenación y servir exactamente la forma que una página o API necesita. CQRS permite que el lado de escritura se mantenga estricto y orientado al dominio, mientras que el lado de lectura se mantiene pragmático y orientado a DTOs. Microsoft recomienda explícitamente un modelo de escritura centrado en la validación y la consistencia, y un modelo de lectura centrado en DTOs o proyecciones optimizadas para presentación y capacidad de respuesta.

También hay un beneficio a nivel de equipo. Three Dots Labs argumentan que dividir comandos y consultas mejora el desacoplamiento, hace que el flujo de ejecución sea más claro y acelera la incorporación porque los desarrolladores pueden inspeccionar una pequeña lista de comandos y consultas disponibles en lugar de perseguir la lógica a través de capas de servicio aleatorias. Microsoft también señala que CQRS es especialmente útil en entornos colaborativos donde múltiples usuarios actualizan los mismos datos y los comandos necesitan suficiente granularidad para prevenir o resolver conflictos.

Mi opinión ligeramente sesgada es esta: la mayoría de los equipos adoptan CQRS demasiado tarde, después de que un “servicio” ya se haya convertido en un monolito de centro blando. Pero muchos equipos también lo adoptan demasiado temprano, principalmente porque el diagrama de arquitectura parecía costoso y, por lo tanto, serio. El momento adecuado es cuando las lecturas y las escrituras están claramente separándose en forma, velocidad o reglas, no cuando tu aplicación de tareas tiene aspiraciones.

Los beneficios y el costo

El CQRS básico tiene beneficios reales incluso antes de añadir cualquier mensajería o almacenes separados. Te da modelos de comando más pequeños, modelos de consulta más pequeños, casos de uso más claros y lugares más obvios para aplicar preocupaciones transversales como registro y instrumentación. Three Dots Labs destacan explícitamente una mejor organización del código, desacoplamiento y modelos más simples como victorias inmediatas, mientras que Microservices.io resalta modelos de comando y consulta más simples y el apoyo a vistas de lectura desnormalizadas y escalables.

Una vez que el problema lo justifica, CQRS también abre la puerta a una optimización más fuerte del lado de lectura. Las directrices de Microsoft señalan que los modelos de lectura separados pueden usar DTOs, proyecciones, réplicas de solo lectura o incluso una tecnología de almacenamiento completamente diferente. También apunta a las vistas materializadas como una forma de evitar uniones pesadas y rutas de consulta con ORM pesado. Si estás evaluando qué capa de acceso a datos usar en el lado de escritura, Comparando ORMs de Go para PostgreSQL cubre las compensaciones entre GORM, Ent, Bun y sqlc en términos prácticos. Es ahí donde CQRS comienza a dar frutos operativamente, no solo estructuralmente.

El costo es igualmente real. La advertencia de Fowler sigue siendo el punto de partida correcto: para la mayoría de los sistemas, CQRS añade una complejidad riesgosa. Microsoft lista la complejidad aumentada y la consistencia eventual como consideraciones centrales, mientras que Microservices.io añade la duplicación potencial de código y el retraso de replicación en las vistas de lectura. Si divides los almacenes, también heredas la tarea de mantenerlos sincronizados, generalmente a través de eventos, sin depender de una transacción distribuida ordenada entre tu base de datos y tu intermediario.

El event sourcing no elimina ese costo; cambia su forma. Las directrices de CQRS de Microsoft dicen que el event sourcing puede hacer que el almacén de eventos sea la única fuente de verdad y permitirte reconstruir vistas materializadas reproduciendo el historial, mientras que Event Horizon señala la trazabilidad y el registro de auditoría como beneficios principales. Pero Microsoft también advierte que la generación de vistas, la reproducción y el manejo de eventos añaden más complejidad de diseño, y sugiere instantáneas para reducir los costos de reproducción. Por eso prefiero explicar el event sourcing como “CQRS más una segunda decisión difícil”, no como el billete de entrada.

Una regla práctica útil que vale la pena tener en mente es que el CQRS básico es barato mientras que el CQRS distribuido es caro, y confundir las dos conversaciones es una de las formas más comunes en que los equipos terminan con mucha más complejidad de la que el problema nunca requirió.

Una implementación simple de CQRS en Go

Un primer paso sensato en Go es mantener una base de datos y dividir solo la capa de aplicación. Los comandos poseen las reglas de negocio y la persistencia. Las consultas devuelven modelos de lectura formados para los llamantes. Este es exactamente el tipo de CQRS básico que Three Dots Labs recomiendan antes de recurrir a buses asíncronos o almacenes de lectura separados.

Comienza con los comandos

package blog

import (
	"context"
	"errors"
	"time"
)

type PublishPostCommand struct {
	Title   string
	Slug    string
	BodyMD  string
	Author  string
}

type PostRepository interface {
	NextID(ctx context.Context) (string, error)
	Save(ctx context.Context, post Post) error
}

type Post struct {
	ID          string
	Title       string
	Slug        string
	BodyMD      string
	Author      string
	PublishedAt time.Time
}

type PublishPostHandler struct {
	Repo  PostRepository
	Now   func() time.Time
}

func (h PublishPostHandler) Handle(ctx context.Context, cmd PublishPostCommand) error {
	if cmd.Title == "" || cmd.Slug == "" || cmd.BodyMD == "" {
		return errors.New("title, slug, and body are required")
	}

	id, err := h.Repo.NextID(ctx)
	if err != nil {
		return err
	}

	post := Post{
		ID:          id,
		Title:       cmd.Title,
		Slug:        cmd.Slug,
		BodyMD:      cmd.BodyMD,
		Author:      cmd.Author,
		PublishedAt: h.Now(),
	}

	return h.Repo.Save(ctx, post)
}

Este manejador no intenta servir una página, dar forma a una respuesta de lista ni optimizar SQL para una cuadrícula de tarjetas. Simplemente aplica la intención y persiste un agregado válido. Ese es el lado del comando haciendo un solo trabajo bien.

Añade consultas

package blog

import "context"

type PostView struct {
	ID          string
	Title       string
	Slug        string
	Author      string
	PublishedAt string
	Excerpt     string
}

type LatestPostsQuery struct {
	Limit int
}

type PostReadModel interface {
	Latest(ctx context.Context, limit int) ([]PostView, error)
	BySlug(ctx context.Context, slug string) (PostView, error)
}

type LatestPostsHandler struct {
	ReadModel PostReadModel
}

func (h LatestPostsHandler) Handle(ctx context.Context, q LatestPostsQuery) ([]PostView, error) {
	limit := q.Limit
	if limit <= 0 {
		limit = 10
	}
	return h.ReadModel.Latest(ctx, limit)
}

type GetPostBySlugQuery struct {
	Slug string
}

type GetPostBySlugHandler struct {
	ReadModel PostReadModel
}

func (h GetPostBySlugHandler) Handle(ctx context.Context, q GetPostBySlugQuery) (PostView, error) {
	return h.ReadModel.BySlug(ctx, q.Slug)
}

Observa que el lado de lectura devuelve un PostView, no el modelo de escritura. Eso refleja la recomendación de Microsoft de que el modelo de lectura esté optimizado para DTOs y presentación, mientras que el modelo de escritura está ajustado para la integridad transaccional y las reglas de dominio.

Conéctalo como una aplicación Go, no como un santuario

package app

import "your/module/internal/blog"

type Application struct {
	Commands Commands
	Queries  Queries
}

type Commands struct {
	PublishPost blog.PublishPostHandler
}

type Queries struct {
	LatestPosts   blog.LatestPostsHandler
	GetPostBySlug blog.GetPostBySlugHandler
}

Esa forma no es accidental. Three Dots Labs usan un patrón muy similar en Wild Workouts: un tipo Application que expone Commands y Queries, con manejadores concretos conectados desde paquetes app/command y app/query separados. Su código de composición de servicios importa esos paquetes por separado y construye un único objeto de aplicación a partir de ellos. Es una forma limpia y propia de Go de hacer que el límite sea obvio sin Drama de Frameworks. Si tu gráfico de dependencias se vuelve complejo a medida que los manejadores se multiplican, Inyección de Dependencias en Go cubre Wire, Dig y patrones de inyección de constructores que se componen naturalmente con esta estructura basada en manejadores.

Si más tarde necesitas comandos asíncronos, eventos entre servicios o un índice de búsqueda desnormalizado, puedes añadirlos desde esta línea base. Three Dots Labs presentan explícitamente buses de comandos asíncronos y bases de datos de consulta separadas como optimizaciones posteriores, no como el punto de partida.

Librerías de Go que vale la pena conocer

El ecosistema de CQRS de Go es más estrecho que el de .NET, lo cual es honestamente una bendición. Puedes revisar las opciones reales en una tarde y evitar adoptar tres abstracciones que no necesitas.

Watermill

Watermill es la elección moderna más clara cuando quieres CQRS más mensajería. Su componente CQRS es una API de alto nivel que te permite trabajar con structs de Go en lugar de mensajes crudos, y sus bloques de construcción incluyen un EventBus, EventProcessor, CommandBus y CommandProcessor. La documentación también cubre grupos de manejadores de eventos para procesamiento ordenado en temas compartidos, un ejemplo de modelo de lectura y metadatos de marshaling personalizados. Fuera de la capa CQRS, Watermill soporta una amplia gama de back-ends de pub/sub, incluidos RabbitMQ, Kafka, NATS Jetstream, Redis Streams, Google Cloud Pub/Sub, SQL, HTTP y otros. Pkg.go.dev marca Watermill como listo para producción con una API pública estable desde la v1.0.0, y la versión del módulo publicada actualmente es la v1.5.2, con GitHub listando esa versión el 13 de mayo.

commandBus, err := cqrs.NewCommandBusWithConfig(pub, cfg)
eventBus, err := cqrs.NewEventBusWithConfig(pub, cfg)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cfg)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cfg)

Usa Watermill cuando los comandos y eventos necesitan cruzar límites de proceso, cuando quieres que la semántica de reintentos y reentrega sean de primera clase, o cuando sabes que tu servicio “simple” ya está a mitad de camino hacia una realidad impulsada por eventos. El inconveniente es que ahora estás teniendo conversaciones sobre intermediarios, temas, ordenación e idempotencia quieras o no. Eso no es un defecto de Watermill. Ese es el costo del espacio de problemas.

Event Horizon

Event Horizon es un kit de herramientas de CQRS y event sourcing para Go. Sus mantenedores lo describen como usado en sistemas de producción, pero también señalan que la API no es final. El kit de herramientas proporciona ayudantes de registro de agregados, comandos y eventos, implementaciones oficiales de almacén de eventos para variantes de memoria y MongoDB, soporte para proyecciones y repositorios, y ejemplos que incluyen una aplicación basada en el patrón de caja de salida (outbox). El flujo de lanzamientos sigue activo, con GitHub mostrando la v0.17.0 el 16 de junio y lanzamientos anteriores añadiendo características como instantáneas, proyecciones reintentables, programación de comandos persistente y el patrón de caja de salida.

eh.RegisterAggregate(func(id uuid.UUID) eh.Aggregate {
	return &InvoiceAggregate{ID: id}
})

eh.RegisterCommand(func() eh.Command {
	return &CreateInvoiceCommand{}
})

Event Horizon tiene más sentido cuando el event sourcing es el punto, no una extensión opcional futura. Si quieres flujos amigables para auditoría, historial reproducible, proyecciones y un modelo centrado en el almacén de eventos, es una opción seria. Si solo quieres servicios de aplicación más limpios en un monolito, probablemente sea más maquinaria de la que necesitas. La nota de “la API no es final” también significa que debes presupuestar un poco más de adaptación con el tiempo que con Watermill.

Go-MediatR

Go-MediatR no es un framework completo de CQRS, pero es útil para CQRS intraproceso. Su README lo describe como una implementación del patrón mediador usada con CQRS, con despacho de solicitud/respuesta para comandos y consultas, despacho de notificación para eventos y comportamientos de canal para preocupaciones transversales. El proyecto también tiene lanzamientos etiquetados, con GitHub listando la v1.4.0 como el lanzamiento más reciente y destacando el registro de manejadores seguro para subprocesos y mejoras relacionadas con la concurrencia.

resp, err := mediatr.Send[*CreateProductCommand, *CreateProductResponse](ctx, cmd)
post, err := mediatr.Send[*GetPostBySlugQuery, *PostView](ctx, query)

Este es un buen ajuste si quieres comandos y consultas basados en manejadores, pero no un intermediario, motor de proyección o almacén de eventos. Es especialmente amigable para equipos que vienen de MediatR en .NET. La compensación es igualmente clara: aún tienes que diseñar tu propia persistencia, estrategia de actualización del modelo de lectura e historia de integración fuera de proceso. En otras palabras, te da el límite de la aplicación, no toda la arquitectura.

Frameworks más antiguos y material de referencia

Hay librerías de CQRS de Go más antiguas que siguen siendo instructivas, pero las trataría como material de referencia antes que como predeterminados para proyectos nuevos.

jetbasrawi/go.cqrs se describe a sí mismo como una implementación de referencia de CQRS en Go con aplicaciones de muestra basadas en los principios de Greg Young. Sin embargo, pkg.go.dev muestra ningún go.mod válido, ninguna versión etiquetada y ninguna versión estable, mientras que GitHub muestra ningún lanzamiento y los metadatos del paquete fueron publicados hace 7.4 años. Esa es una historia útil, no una señal fuerte para una adopción de producción fresca en 2026.

andrewwebber/cqrs es similar: proporciona event sourcing, emisión y procesamiento de comandos, publicación de eventos y generación de modelos de lectura a partir de eventos publicados, pero los metadatos del paquete también fueron publicados hace 7.4 años. Lo leería absolutamente si quieres entender cómo las librerías de CQRS de Go anteriores abordaban el problema. Sería cauteloso sobre hacerlo la base de un nuevo código base a menos que estés dispuesto a convertirte en mantenedor a tiempo parcial de tu propia pila de arquitectura.

Un diseño práctico de proyecto en Go

Un diseño típico de CQRS en Go debería hacer que los casos de uso sean obvios, no enterrarlos bajo abstracciones genéricas. Wild Workouts es una buena referencia aquí. El repositorio separa contextos delimitados bajo internal, mantiene comandos y consultas en paquetes de aplicación distintos y los conecta a un tipo Application que expone Commands y Queries. La composición de servicios junta adaptadores, manejadores y dependencias explícitamente. Los patrones descritos aquí se alinean con las directrices más amplias en Estructura de Proyectos Go: Prácticas y Patrones, que cubren el conjunto más amplio de decisiones de diseño que enfrentan los equipos a medida que los códigos Go crecen.

Un diseño pragmático se ve así:

internal/
  blog/
    app/
      app.go
      command/
        publish_post.go
        unpublish_post.go
      query/
        get_post_by_slug.go
        latest_posts.go
    domain/
      post.go
      slug.go
    adapters/
      postgres/
        post_repository.go
        post_read_model.go
    ports/
      http/
        handler.go
    service/
      application.go

Este diseño tiene algunas ventajas.

Primero, los manejadores de comandos y consultas viven cerca de los casos de uso que implementan. Eso hace que sea más difícil ocultar el comportamiento del negocio en repositorios o manejadores nombrados después de capas de transporte. Three Dots Labs hacen esto directamente en Wild Workouts, donde app/command y app/query son paquetes separados y el Application de nivel superior agrupa manejadores por responsabilidad.

Segundo, el paquete de dominio puede mantenerse enfocado en invariantes y comportamiento, mientras que el lado de la consulta es libre de devolver DTOs y proyecciones. Eso se alinea con las directrices de Microsoft para modelos de escritura y lectura y evita la anti-patrón común de CQRS donde el lado de la consulta se ve obligado a volver a través de objetos de dominio solo por pureza ideológica.

Tercero, esta estructura escala desde el CQRS más pequeño útil hasta variantes más pesadas. Puedes mantener una base de datos PostgreSQL y dos implementaciones de repositorio hoy, y luego añadir un índice de búsqueda o una proyección de lectura impulsada por eventos más tarde sin tener que reescribir toda la forma de la aplicación. Three Dots Labs describen explícitamente esa progresión desde el CQRS básico hasta buses de comandos asíncronos y almacenes de consulta separados solo cuando el sistema los necesita.

Cuándo CQRS encaja y cuándo no

CQRS tiene sentido cuando las lecturas y las escrituras son verdaderamente problemas diferentes. Microsoft lo recomienda para cargas de trabajo donde los modelos de lectura y escritura necesitan optimización independiente, donde múltiples usuarios colaboran en los mismos datos y donde una separación clara ayuda con el rendimiento, escalabilidad y seguridad. Microservices.io añade otro ajuste clásico: vistas desnormalizadas de alto rendimiento construidas a partir de eventos de dominio o proyecciones materializadas. Three Dots Labs también señalan la lógica de negocio compleja, mantenibilidad y extensión futura hacia comandos asíncronos o almacenes de lectura especializados como razones fuertes para adoptarlo en Go.

En la práctica, eso a menudo significa sistemas con reglas de dominio ricas, modelos de lectura costosos, vistas de informes que no se mapean limpiamente a agregados, o microservicios que publican eventos y construyen proyecciones en otro lugar. En esos contextos, el Patrón Saga para transacciones distribuidas a menudo aparece junto con CQRS como el mecanismo de coordinación para operaciones de negocio de múltiples pasos que abarcan límites de servicio. También encaja en productos donde el lado de escritura debe ser estricto y auditable mientras que el lado de lectura debe ser rápido y formado para consumo de UI o API. Si ya estás hablando de proyecciones, réplicas o reconstrucción de vistas a partir de eventos, probablemente estás en el territorio de CQRS uses la etiqueta o no.

CQRS no tiene sentido cuando tu servicio es un editor de datos sencillo. Fowler dice outright que para la mayoría de los sistemas CQRS añade una complejidad riesgosa, y Three Dots Labs dicen que los servicios CRUD simples que reciben y devuelven esencialmente los mismos datos no son un buen ajuste. En su propio ejemplo de Wild Workouts, un servicio de usuarios más simple no usa Arquitectura Limpia y CQRS porque los patrones no pagarían su alquiler allí.

Esa es la parte que vale la pena decir claramente en un blog técnico: CQRS no es una insignia de madurez sino un intercambio deliberado, y solo tiene sentido cuando realmente necesitas lo que te da. Si tu panel de administración escribe filas y lee las mismas filas de vuelta, no separe el modelo solo porque puedes. Si tus manejadores de comandos son principalmente “establecer campo X en registro Y”, no tienes un problema de CQRS. Tienes una aplicación normal, y ese es un software perfectamente respetable.

Reflexiones finales

La mejor manera de implementar CQRS en Go es comenzar con la versión aburrida. Separa los manejadores de comandos de los manejadores de consultas. Deja que los comandos modelen la intención del negocio. Deja que las consultas devuelvan modelos de lectura. Mantén la misma base de datos si eso es todo lo que necesitas. Luego, solo cuando el sistema te obligue a actuar, añade buses asíncronos, proyecciones, almacenes separados o event sourcing. Esa progresión es consistente con la advertencia de Fowler sobre la complejidad, las directrices escalonadas de CQRS de Microsoft y los ejemplos pragmáticos de Go de Three Dots Labs.

Si necesitas una librería, Watermill es la elección de propósito general más fuerte para CQRS impulsado por mensajes en Go, Event Horizon es convincente cuando el event sourcing es el centro de gravedad, y Go-MediatR es un buen toque ligero cuando solo necesitas despacho de comandos y consultas intraproceso. Todo lo demás debería ganarse su lugar muy cuidadosamente. Para un mapa más amplio de estructura de código, integración y patrones de acceso a datos en sistemas Go de producción, la Guía de Arquitectura de Aplicaciones es un compañero útil.

Eso, al final, es la respuesta más propia de Go a CQRS: usa el patrón, no el disfraz.

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.