Implementando CQRS em Go: Um Guia Prático para Arquitetura Escalável
Construa CQRS em Go sem cerimonial desnecessário
O CQRS é um daqueles padrões que é frequentemente superdimensionado, complicador e, ocasionalmente, diagnosticado erroneamente como uma cura para o tédio do bom e velho CRUD.
A versão útil é muito mais simples: separe o código que altera o estado do código que lê o estado e permita que cada lado evolua para sua própria função. Martin Fowler descreve o CQRS como o uso de um modelo diferente para atualizar informações em relação ao usado para ler, ao mesmo tempo em que alerta que, para a maioria dos sistemas, isso adiciona complexidade arriscada. A Microsoft faz o mesmo ponto central em termos mais operacionais: separe os modelos de leitura e escrita para que cada um possa ser otimizado independentemente.

Se você trabalha com Go, essa ideia se encaixa de forma incomum na linguagem. O Go é bom em fronteiras explícitas, interfaces pequenas, tipos de dados simples e pacotes orientados a casos de uso. Isso torna o CQRS básico em Go muito menos teatral do que frequentemente parece em slides de conferências. Você não precisa de event sourcing, Kafka ou três bancos de dados para começar. Na verdade, tanto as orientações da Microsoft sobre CQRS quanto os exemplos da Three Dots Labs em Go mostram que uma implementação simples pode compartilhar o mesmo armazenamento subjacente, com manipuladores de comandos e consultas separados adicionados primeiro e infraestrutura mais sofisticada introduzida apenas quando o problema realmente exigir.
O que CQRS Significa Realmente
No núcleo, o CQRS traça uma linha rígida entre comandos e consultas. Uma consulta lê dados e não deve modificar o estado do sistema. Um comando altera o estado e não deve retornar dados de domínio como seu principal resultado. A Three Dots Labs expressa isso em termos práticos de Go: consultas retornam dados e comandos fazem alterações, com erros sendo um resultado normal de comando. Esse é o movimento básico. Tudo o mais é opcional.
Um equívoco comum é que o CQRS significa automaticamente bancos de dados separados, projeções assíncronas ou event sourcing. Isso não é verdade. O guia de padrões da Microsoft trata explicitamente de armazenamentos de dados separados como a forma mais avançada, não a padrão, e a Three Dots Labs mostra uma implementação em Go onde as consultas leem do mesmo banco de dados que as escritas, pois isso é suficiente para o sistema em questão. Se seu artigo ensinar apenas uma coisa claramente, que seja esta: o CQRS é primordialmente uma escolha de modelagem e estrutura de aplicação, não um pacote obrigatório de sistemas distribuídos.
O outro detalhe importante é a nomenclatura. Os comandos devem modelar a intenção de negócios, não mutações de armazenamento. O exemplo da Microsoft contrasta “Reservar quarto de hotel” com “Definir Status da Reserva como Reservado”, e a Three Dots Labs recomenda nomes próximos à maneira como os especialistas do domínio falam, como “AgendarTreino” ou “CancelarTreino” em vez de verbos genéricos como “Criar” e “Excluir”. Em Go, essa disciplina de nomenclatura rende frutos, pois os nomes dos comandos frequentemente se tornam nomes de tipos, nomes de manipuladores e limites de pacotes.
Por Que as Equipes Buscam Isso
O CQRS se torna atraente quando um único modelo CRUD começa a desempenhar muitos trabalhos mal. As orientações da Microsoft listam os pontos de pressão habituais: as representações de leitura e escrita dos mesmos dados divergem, atualizações concorrentes criam contenção de bloqueio, o desempenho de leitura sofre sob a complexidade da consulta e as entidades compartilhadas transformam as regras de segurança em uma teia. Em outras palavras, o problema não é que o CRUD seja moralmente errado. O problema é que um único modelo está sendo forçado a satisfazer preocupações incompatíveis simultaneamente.
Isso é especialmente comum em produtos técnicos. As escritas tendem a se preocupar com validação, invariantes, transações e regras de negócios. As leituras tendem a se preocupar com filtros, junções, agregação, cache, ordenação e servir exatamente a forma que uma página ou API precisa. O CQRS permite que o lado da escrita permaneça rigoroso e orientado ao domínio, enquanto o lado da leitura permanece pragmático e orientado a DTOs. A Microsoft recomenda explicitamente um modelo de escrita focado em validação e consistência, e um modelo de leitura focado em DTOs ou projeções otimizadas para apresentação e resposta.
Há também um benefício no nível da equipe. A Three Dots Labs argumenta que dividir comandos e consultas melhora o desacoplamento, torna o fluxo de execução mais claro e acelera a integração, pois os desenvolvedores podem inspecionar uma pequena lista de comandos e consultas disponíveis em vez de caçar lógica através de camadas de serviço aleatórias. A Microsoft observa similarmente que o CQRS é especialmente útil em ambientes colaborativos onde vários usuários atualizam os mesmos dados e os comandos precisam de granularidade suficiente para prevenir ou resolver conflitos.
Minha opinião levemente pessoal é esta: a maioria das equipes adota o CQRS tarde demais, depois que um “serviço” já se transformou em um monolito de centro mole. Mas muitas equipes também o adotam muito cedo, principalmente porque o diagrama de arquitetura parecia caro e, portanto, sério. O momento certo é quando as leituras e as escritas estão claramente se afastando em forma, velocidade ou regras, não quando sua aplicação de lista de tarefas tem aspirações.
Os Benefícios e o Custo
O CQRS básico tem benefícios reais mesmo antes de adicionar qualquer mensagem ou armazenamentos separados. Ele oferece modelos de comando menores, modelos de consulta menores, casos de uso mais claros e lugares mais óbvios para aplicar preocupações transversais como registro e instrumentação. A Three Dots Labs destaca explicitamente melhor organização de código, desacoplamento e modelos mais simples como vitórias imediatas, enquanto o Microservices.io destaca modelos de comando e consulta mais simples e suporte para visualizações de leitura desnormalizadas e escaláveis.
Uma vez que o problema justifique, o CQRS também abre a porta para uma otimização mais forte do lado da leitura. As orientações da Microsoft observam que modelos de leitura separados podem usar DTOs, projeções, réplicas somente leitura ou até mesmo uma tecnologia de armazenamento completamente diferente. Ela também aponta para visualizações materializadas como uma maneira de evitar junções pesadas e caminhos de consulta pesados em ORM. Se você está avaliando qual camada de acesso a dados usar no lado da escrita, Comparando ORMs Go para PostgreSQL cobre as compensações entre GORM, Ent, Bun e sqlc em termos práticos. É onde o CQRS começa a dar frutos operacionalmente, não apenas estruturalmente.
O custo é igualmente real. O aviso de Fowler ainda é o ponto de partida correto: para a maioria dos sistemas, o CQRS adiciona complexidade arriscada. A Microsoft lista complexidade aumentada e consistência eventual como considerações principais, enquanto o Microservices.io adiciona duplicação potencial de código e atraso de replicação nas visualizações de leitura. Se você dividir os armazenamentos, também herdará a tarefa de mantê-los sincronizados, geralmente por meio de eventos, sem depender de uma transação distribuída organizada entre seu banco de dados e o broker.
O event sourcing não remove esse custo; ele muda sua forma. As orientações de CQRS da Microsoft dizem que o event sourcing pode tornar o armazenamento de eventos a única fonte de verdade e permitir que você reconstrua visualizações materializadas reproduzindo o histórico, enquanto o Event Horizon aponta para rastreabilidade e registro de auditoria como benefícios principais. Mas a Microsoft também alerta que a geração de visualizações, a reprodução e o tratamento de eventos adicionam mais complexidade de design e sugere instantâneos para reduzir os custos de reprodução. É por isso que prefiro explicar o event sourcing como “CQRS mais uma segunda decisão difícil”, não como o bilhete de entrada.
Uma regra prática útil para ter em mente é que o CQRS básico é barato, enquanto o CQRS distribuído é caro, e confundir as duas conversas é uma das maneiras mais comuns pelas quais as equipes acabam com muito mais complexidade do que o problema jamais exigiu.
Uma Implementação Simples de CQRS em Go
Um primeiro passo sensato em Go é manter um banco de dados e dividir apenas a camada de aplicação. Os comandos possuem as regras de negócios e a persistência. As consultas retornam modelos de leitura moldados para os chamadores. Este é exatamente o tipo de CQRS básico que a Three Dots Labs recomenda antes de buscar barramentos assíncronos ou armazenamentos de leitura separados.
Comece com comandos
package blog
import (
"context"
"errors"
"time"
)
type PublishPostCommand struct {
Título string
Slug string
CorpoMD string
Autor string
}
type PostRepository interface {
PróximoID(ctx context.Context) (string, error)
Salvar(ctx context.Context, post Post) error
}
type Post struct {
ID string
Título string
Slug string
CorpoMD string
Autor string
PublicadoEm time.Time
}
type PublishPostHandler struct {
Repo PostRepository
Agora func() time.Time
}
func (h PublishPostHandler) Handle(ctx context.Context, cmd PublishPostCommand) error {
if cmd.Título == "" || cmd.Slug == "" || cmd.CorpoMD == "" {
return errors.New("título, slug e corpo são obrigatórios")
}
id, err := h.Repo.PróximoID(ctx)
if err != nil {
return err
}
post := Post{
ID: id,
Título: cmd.Título,
Slug: cmd.Slug,
CorpoMD: cmd.CorpoMD,
Autor: cmd.Autor,
PublicadoEm: h.Agora(),
}
return h.Repo.Salvar(ctx, post)
}
Este manipulador não tenta servir uma página, moldar uma resposta de lista ou otimizar SQL para uma grade de cartões. Ele apenas impõe a intenção e persiste um agregado válido. Esse é o lado do comando fazendo um trabalho bem.
Adicione consultas
package blog
import "context"
type PostView struct {
ID string
Título string
Slug string
Autor string
PublicadoEm string
Resumo string
}
type LatestPostsQuery struct {
Limite int
}
type PostReadModel interface {
MaisRecentes(ctx context.Context, limite int) ([]PostView, error)
PorSlug(ctx context.Context, slug string) (PostView, error)
}
type LatestPostsHandler struct {
ModeloDeLeitura PostReadModel
}
func (h LatestPostsHandler) Handle(ctx context.Context, q LatestPostsQuery) ([]PostView, error) {
limite := q.Limite
if limite <= 0 {
limite = 10
}
return h.ModeloDeLeitura.MaisRecentes(ctx, limite)
}
type GetPostBySlugQuery struct {
Slug string
}
type GetPostBySlugHandler struct {
ModeloDeLeitura PostReadModel
}
func (h GetPostBySlugHandler) Handle(ctx context.Context, q GetPostBySlugQuery) (PostView, error) {
return h.ModeloDeLeitura.PorSlug(ctx, q.Slug)
}
Observe que o lado da leitura retorna um PostView, não o modelo de escrita. Isso espelha a recomendação da Microsoft de que o modelo de leitura seja otimizado para DTOs e apresentação, enquanto o modelo de escrita é ajustado para integridade transacional e regras de domínio.
Conecte como uma aplicação Go, não um santuário
package app
import "seu/módulo/internal/blog"
type Application struct {
Comandos Comandos
Consultas Consultas
}
type Comandos struct {
PublicarPost blog.PublishPostHandler
}
type Consultas struct {
MaisRecentes blog.LatestPostsHandler
ObterPostPorSlug blog.GetPostBySlugHandler
}
Essa forma não é acidental. A Three Dots Labs usa um padrão muito semelhante no Wild Workouts: um tipo Application expondo Commands e Queries, com manipuladores concretos conectados a partir de pacotes app/command e app/query separados. O código de composição de serviço importa esses pacotes separadamente e constrói um único objeto de aplicação a partir deles. É uma maneira limpa e “goesca” de tornar a fronteira óbvia sem Drama de Framework. Se seu gráfico de dependência ficar complexo à medida que os manipuladores se multiplicam, Injeção de Dependência em Go cobre padrões de Wire, Dig e injeção de construtor que se compõem naturalmente com esta estrutura baseada em manipuladores.
Se você precisar mais tarde de comandos assíncronos, eventos entre serviços ou um índice de busca desnormalizado, você pode adicioná-los a partir desta base. A Three Dots Labs apresenta explicitamente barramentos de comando assíncronos e bancos de dados de consulta separados como otimizações posteriores, não o ponto de partida.
Bibliotecas Go Vale a Pena Conhecer
O ecossistema CQRS do Go é mais estreito do que o do .NET, o que, honestamente, é uma bênção. Você pode revisar as opções reais em uma tarde e evitar adotar três abstrações que não precisa.
Watermill
Watermill é a escolha moderna mais clara quando você quer CQRS mais mensagens. Seu componente CQRS é uma API de alto nível que permite trabalhar com structs Go em vez de mensagens brutas, e seus blocos de construção incluem EventBus, EventProcessor, CommandBus e CommandProcessor. A documentação também cobre grupos de manipuladores de eventos para processamento ordenado em tópicos compartilhados, um exemplo de modelo de leitura e metadados de marshaling personalizados. Fora da camada CQRS, o Watermill suporta uma ampla gama de back-ends pub/sub, incluindo RabbitMQ, Kafka, NATS Jetstream, Redis Streams, Google Cloud Pub/Sub, SQL, HTTP e outros. O Pkg.go.dev marca o Watermill como pronto para produção com uma API pública estável desde a v1.0.0, e a versão do módulo publicada atualmente é a v1.5.2, com o GitHub listando esse lançamento em 13 de maio.
commandBus, err := cqrs.NewCommandBusWithConfig(pub, cfg)
eventBus, err := cqrs.NewEventBusWithConfig(pub, cfg)
commandProcessor, err := cqrs.NewCommandProcessorWithConfig(router, cfg)
eventProcessor, err := cqrs.NewEventProcessorWithConfig(router, cfg)
Use o Watermill quando comandos e eventos precisarem cruzar limites de processo, quando você quiser que retrabalhos e semânticas de reentrega sejam de primeira classe, ou quando você souber que seu serviço “simples” já está meio caminho para a realidade orientada a eventos. A desvantagem é que você agora está tendo conversas sobre broker, tópico, ordenação e idempotência quer queira ou não. Isso não é um defeito do Watermill. Esse é o custo do espaço de problemas.
Event Horizon
Event Horizon é um toolkit de CQRS e event sourcing para Go. Seus mantenedores descrevem-no como usado em sistemas de produção, mas também observam que a API não é final. O toolkit fornece agregado, comandos e auxiliares de registro de eventos, implementações oficiais de armazenamento de eventos para variantes de memória e MongoDB, suporte a projeção e repositório, e exemplos que incluem uma aplicação baseada no padrão outbox. O fluxo de lançamento ainda está ativo, com o GitHub mostrando a v0.17.0 em 16 de junho e lançamentos anteriores adicionando recursos como instantâneos, projeções retrabalháveis, agendamento persistente de comandos e o padrão outbox.
eh.RegisterAggregate(func(id uuid.UUID) eh.Aggregate {
return &InvoiceAggregate{ID: id}
})
eh.RegisterCommand(func() eh.Command {
return &CreateInvoiceCommand{}
})
O Event Horizon faz mais sentido quando o event sourcing é o ponto, não uma extensão futura opcional. Se você quer streams amigáveis para auditoria, histórico reproduzível, projeções e um modelo centrado no armazenamento de eventos, é uma opção séria. Se você quer apenas serviços de aplicação mais limpos em um monolito, provavelmente é mais maquinaria do que você precisa. A nota “a API não é final” também significa que você deve orçar para uma adaptação um pouco maior ao longo do tempo do que faria com o Watermill.
Go-MediatR
Go-MediatR não é um framework CQRS completo, mas é útil para CQRS intra-processo. Seu README descreve-o como uma implementação do padrão mediator usada com CQRS, com despacho de solicitação/resposta para comandos e consultas, despacho de notificação para eventos e comportamentos de pipeline para preocupações transversais. O projeto também tem lançamentos marcados, com o GitHub listando a v1.4.0 como o lançamento mais recente e destacando registro de manipuladores seguro para threads e melhorias relacionadas à concorrência.
resp, err := mediatr.Send[*CreateProductCommand, *CreateProductResponse](ctx, cmd)
post, err := mediatr.Send[*GetPostBySlugQuery, *PostView](ctx, query)
Este é um bom ajuste se você quer comandos e consultas baseados em manipuladores, mas não um broker, motor de projeção ou armazenamento de eventos. É especialmente amigável para equipes vindas do MediatR no .NET. A compensação é igualmente clara: você ainda tem que projetar sua própria persistência, estratégia de atualização do modelo de leitura e história de integração fora do processo. Em outras palavras, ele dá a você o limite da aplicação, não toda a arquitetura.
Frameworks mais antigos e material de referência
Há bibliotecas Go CQRS mais antigas que ainda são instrutivas, mas eu as trataria como material de referência antes de tratá-las como padrões para novos projetos.
jetbasrawi/go.cqrs descreve-se como uma implementação de referência Go CQRS com aplicações de amostra baseadas nos princípios de Greg Young. No entanto, o pkg.go.dev mostra nenhum go.mod válido, nenhuma versão marcada e nenhuma versão estável, enquanto o GitHub mostra nenhum lançamento e os metadados do pacote foram publicados há 7,4 anos. Isso é história útil, não um sinal forte para uma adoção de produção fresca em 2026.
andrewwebber/cqrs é similar: fornece event sourcing, emissão e processamento de comandos, publicação de eventos e geração de modelo de leitura a partir de eventos publicados, mas os metadados do pacote também foram publicados há 7,4 anos. Eu certamente o leria se você quiser entender como as bibliotecas Go CQRS anteriores abordavam o problema. Eu seria cauteloso em torná-lo a base de uma nova base de código a menos que você esteja feliz em se tornar mantenedor em meio período da sua própria pilha de arquitetura.
Um Layout de Projeto Go Prático
Um layout típico de CQRS em Go deve tornar os casos de uso óbvios, não enterrá-los sob abstrações genéricas. Wild Workouts é uma boa referência aqui. O repositório separa contextos delimitados sob internal, mantém comandos e consultas em pacotes de aplicação distintos e conecta-os a um tipo Application expondo Commands e Queries. A composição de serviço reúne adaptadores, manipuladores e dependências explicitamente. Os padrões descritos aqui se alinham com as orientações mais amplas em Estrutura de Projeto Go: Práticas & Padrões, que cobre o conjunto mais amplo de decisões de layout que as equipes enfrentam à medida que as bases de código Go crescem.
Um layout pragmático parece com isto:
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 layout tem algumas vantagens.
Primeiro, os manipuladores de comando e consulta vivem próximos aos casos de uso que implementam. Isso torna mais difícil esconder comportamento de negócios em repositórios ou manipuladores nomeados após camadas de transporte. A Three Dots Labs faz isso diretamente no Wild Workouts, onde app/command e app/query são pacotes separados e o Application de nível superior agrupa manipuladores por responsabilidade.
Segundo, o pacote de domínio pode permanecer focado em invariantes e comportamento, enquanto o lado da consulta é livre para retornar DTOs e projeções. Isso se alinha com as orientações da Microsoft sobre modelo de escrita e modelo de leitura e evita o anti-padrão CQRS comum onde o lado da consulta é forçado de volta através de objetos de domínio apenas por pureza ideológica.
Terceiro, esta estrutura escala do CQRS mais pequeno e útil para variantes mais pesadas. Você pode manter um banco de dados PostgreSQL e duas implementações de repositório hoje, e depois adicionar um índice de busca ou projeção de leitura orientada a eventos mais tarde sem ter que reescrever toda a forma da aplicação. A Three Dots Labs descreve explicitamente essa progressão do CQRS básico para barramentos de comando assíncronos e armazenamentos de consulta separados apenas quando o sistema os precisa.
Quando CQRS Cabe e Quando Não
O CQRS faz sentido quando leituras e escritas são problemas verdadeiramente diferentes. A Microsoft recomenda para cargas de trabalho onde os modelos de leitura e escrita precisam de otimização independente, onde múltiplos usuários colaboram nos mesmos dados e onde uma separação clara ajuda com desempenho, escalabilidade e segurança. O Microservices.io adiciona outro ajuste clássico: visualizações desnormalizadas de alto desempenho construídas a partir de eventos de domínio ou projeções materializadas. A Three Dots Labs também aponta para lógica de negócios complexa, manutenibilidade e extensão futura para comandos assíncronos ou armazenamentos de leitura especializados como razões fortes para adotá-lo em Go.
Na prática, isso geralmente significa sistemas com regras de domínio ricas, modelos de leitura caros, visualizações de relatórios que não mapeiam bem para agregados ou microsserviços que publicam eventos e constroem projeções em outro lugar. Nesses contextos, o padrão Saga para transações distribuídas frequentemente aparece ao lado do CQRS como o mecanismo de coordenação para operações de negócios em múltiplos passos que atravessam limites de serviço. Também se ajusta a produtos onde o lado da escrita deve ser rigoroso e auditável, enquanto o lado da leitura deve ser rápido e moldado para consumo de UI ou API. Se você já está falando sobre projeções, réplicas ou reconstrução de visualizações a partir de eventos, provavelmente está no território do CQRS, quer use o rótulo ou não.
O CQRS não faz sentido quando seu serviço é um editor de dados direto. Fowler diz claramente que para a maioria dos sistemas o CQRS adiciona complexidade arriscada, e a Three Dots Labs diz que serviços CRUD simples que recebem e retornam essencialmente os mesmos dados não são um bom ajuste. Em seu próprio exemplo Wild Workouts, um serviço de usuários mais simples não usa Clean Architecture e CQRS porque os padrões não pagariam seu aluguel lá.
Essa é a parte que vale a pena dizer claramente em um blog técnico: o CQRS não é um distintivo de maturidade, mas um trade-off deliberado, e só faz sentido quando você realmente precisa do que ele oferece. Se seu painel administrativo escreve linhas e lê as mesmas linhas de volta, não separe o modelo apenas porque você pode. Se seus manipuladores de comando são majoritariamente “definir campo X no registro Y”, você não tem um problema de CQRS. Você tem uma aplicação normal, e isso é software perfeitamente respeitável.
Pensamentos Finais
A melhor maneira de implementar CQRS em Go é começar com a versão simples. Separe manipuladores de comando de manipuladores de consulta. Permita que comandos modelem a intenção de negócios. Permita que consultas retornem modelos de leitura. Mantenha o mesmo banco de dados se for tudo o que você precisa. Então, apenas quando o sistema forçar sua mão, adicione barramentos assíncronos, projeções, armazenamentos separados ou event sourcing. Essa progressão é consistente com o aviso de Fowler sobre complexidade, as orientações de CQRS em estágios da Microsoft e os exemplos Go pragmáticos da Three Dots Labs.
Se você precisar de uma biblioteca, o Watermill é a escolha de propósito geral mais forte para CQRS orientado a mensagens em Go, o Event Horizon é atraente quando o event sourcing é o centro de gravidade, e o Go-MediatR é um toque leve bom quando você só precisa de despacho de comando e consulta intra-processo. Tudo o mais deve ganhar seu lugar muito cuidadosamente. Para um mapa mais amplo de estrutura de código, integração e padrões de acesso a dados em sistemas Go de produção, o guia App Architecture é um companheiro útil.
Isso, no final, é a resposta mais “goesca” para o CQRS: use o padrão, não o disfarce.