Em 2010, Greg Young — engenheiro independente, consultor de DDD — publicou um pequeno artigo chamado "CQRS Documents", formalizando um padrão que ele chamou de CQRS: Command Query Responsibility Segregation. A tese era simples: separar o modelo que escreve (commands, mudam estado) do modelo que lê (queries, retornam dados). Cada modelo poderia ser otimizado independentemente — modelo de escrita rico em invariantes do domínio, modelo de leitura otimizado para queries específicas (denormalizado, projeções por uso).
O padrão deriva de uma observação anterior de Bertrand Meyer (Eiffel, 1992) — Command Query Separation (CQS): cada método é ou comando (modifica estado, retorna void) ou query (retorna dado, não modifica estado). CQRS estende CQS para arquitetura — não apenas métodos, mas modelos de dados e camadas inteiras separadas.
CQRS frequentemente vem associado a event sourcing — armazenar não o estado atual mas a sequência de eventos que levaram a ele. Os dois conceitos são distintos mas complementares: CQRS é pattern de arquitetura; event sourcing é pattern de armazenamento. Sistemas podem usar CQRS sem event sourcing (modelo de escrita relacional, modelo de leitura denormalizado), ou event sourcing sem CQRS (sistemas mais raros). A combinação é a forma mais reconhecida em sistemas modernos.
Este conceito articula CQRS em pragmatismo. Onde vale a complicação, onde é overkill, como implementar progressivamente (CQRS "leve" sem event sourcing pleno), e os anti-padrões clássicos. O conceito fecha o módulo 07 conectando com tudo que veio antes — especialmente replicação para escala de leitura (conceito 05) e CAP/PACELC (conceito 07), porque CQRS é em essência uma forma sofisticada de aceitar eventual consistency em troca de escala de leitura.
O problema que CQRS resolve
Em sistemas tradicionais, modelo de domínio (entidades, agregados) serve duas demandas distintas. Para escrita, precisa enforçar invariantes: "Pedido só pode ser cancelado se ainda não enviado", "saldo nunca pode ficar negativo". Para leitura, precisa atender queries diversas: dashboard com totais, listagem com filtros e ordenação, relatórios analíticos.
Servir as duas demandas com mesmo modelo gera tensão. Ricos modelos de domínio com invariantes fortes ficam difíceis de consultar (precisa navegar agregados, JOINs múltiplos). Modelos otimizados para queries (denormalizados, com campos calculados) ficam pobres em invariantes (mudanças podem violar regras se não cuidadas).
A escala adiciona pressão. Em sistema com 1000× mais leituras que escritas (típico em conteúdo, e-commerce, dashboards), o modelo de leitura domina performance — mas o modelo de escrita ainda precisa dos invariantes. Vertical scaling do banco serve até certo ponto; depois, leituras e escritas competem pelos mesmos recursos.
CQRS resolve articulando explicitamente: dois modelos, cada um otimizado para sua tarefa. Modelo de escrita é normalizado, focado em invariantes; modelo de leitura é denormalizado, otimizado por query, possivelmente em banco diferente (Elasticsearch para busca, Redis para counters, columnar para analytics). Sincronização entre os dois acontece via eventos ou replicação, com lag aceitável.
CQRS leve vs CQRS pleno
CQRS pode ser implementado em diferentes níveis de profundidade. Vale conhecer o espectro para adotar gradualmente.
Nível 1 — separação de classes (sem mudança de banco)
Mais simples. Aplicação tem
CommandHandlers separados de
QueryHandlers. Mesmo banco, mesmo
schema. CommandHandlers carregam aggregate, validam
invariantes, persistem. QueryHandlers fazem
query direta (possivelmente raw SQL otimizado),
retornam DTOs.
Ganhos: separação conceitual clara; query handlers podem fazer SQL otimizado sem se preocupar com modelo de domínio rico; facilita testes (commands testáveis com mock de repository). Custos: zero — mesma stack.
Esse nível é o que MediatR + .NET há anos promovem. Se sua equipe está adotando CQRS, este é onde começar.
Nível 2 — banco read-replica para queries
Commands escrevem no primary; queries lêem de replicas. Conceito 05 do módulo cobriu. Eventual consistency natural (lag de replicação).
Ganhos: scale de leitura separada da escrita; primary não saturado por queries pesadas. Custos: lag entre write e query (precisa lidar com read-your-writes; conceito 05 cobre).
Nível 3 — modelos diferentes (mesmo banco)
Tabelas separadas para escrita e leitura. Pedidos.Write (normalizada) e Pedidos.Read (denormalizada). Sincronização por triggers, stored procedures, ou aplicação. Read tem todos os campos calculados, não precisa JOINs.
Ganhos: queries muito mais rápidas; modelo de leitura otimizado para uso. Custos: sincronização adicional; sistema de migrations mais complexo; eventual consistency entre tabelas (mesmo banco).
Nível 4 — bancos diferentes
Modelo de escrita em SQL relacional (Postgres); modelo de leitura em outro store (Elasticsearch para busca, Redis para counters, ClickHouse para analytics). Sincronização via mensageria (Kafka, RabbitMQ).
Ganhos: cada lado usa ferramenta ideal; queries específicas (full-text search, agregações) ficam triviais. Custos: complexidade operacional (dois bancos); pipeline de eventos; eventual consistency cross-store.
Nível 5 — CQRS pleno com event sourcing
Modelo de escrita não armazena estado, armazena eventos (PedidoCriado, ItemAdicionado, PedidoCancelado). Estado atual é derivado por replay. Modelo de leitura é projeção dos eventos.
Ganhos: histórico completo (auditoria nativa); projeções múltiplas (cada query tem seu modelo); replay para corrigir bugs históricos; arquitetura naturalmente event-driven. Custos: complexidade alta; modelo de domínio diferente (eventos como cidadãos primários); event versioning ao longo do tempo é problema; team skill maior.
Sistemas reais frequentemente operam em níveis intermediários. Adotar CQRS pleno quando nível 1-3 basta é overkill comum.
Sincronização — o problema central
Em CQRS com modelos separados, sincronizar é o desafio. Como modelo de leitura "sabe" que modelo de escrita mudou?
Mesmo banco — triggers ou app code
Quando read e write estão no mesmo banco, opções são:
Triggers: ao escrever na tabela Write, trigger atualiza a Read. Síncrono dentro da mesma transação. Garantia de consistência; acoplamento entre tabelas; performance impacta writes.
App code: command handler escreve em Write e Read na mesma transação. Mais explícito; mantenível; pode falhar se schema muda em paralelo.
Materialized views: Postgres tem materialized views que se atualizam sob comando. Útil para projeções analíticas; refresh periódico (não em tempo real).
Bancos diferentes — eventos
Quando read e write estão em bancos diferentes, sincronização inevitavelmente é via eventos.
Outbox pattern. Já visto no módulo 05 conceito 12. Command escreve estado e evento na mesma transação (em tabela "outbox"). Worker separado lê outbox, publica em fila/Kafka, deleta da outbox. Garante at-least-once delivery sem perda.
Change Data Capture (CDC). Sistema externo (Debezium) lê o WAL/binlog do banco e publica eventos. Não exige código de aplicação para emitir eventos; transparente. Cobertura completa (toda mudança gera evento).
Event sourcing nativo. Aplicação grava eventos diretamente em event store (EventStoreDB, Apache Pulsar, Kafka como event store). Leitura projeta dos eventos.
Todos os três introduzem eventual consistency entre write e read. Esse lag é tipicamente milissegundos a segundos; aceitável para a maioria das queries; problemático para read-your-writes (conceito 05 trata).
Read-models / projections
Projection é o nome técnico para "modelo de leitura derivado". Cada projection é otimizada para uma query (ou conjunto pequeno de queries relacionadas).
Projeções típicas:
Listagem com filtros e ordenação.
Tabela com índices nos campos filtrados; campos
pré-calculados (ex.: total_paid
em vez de calcular cada vez).
Busca full-text. Elasticsearch ou similar. Documento por entidade, com campos relevantes indexados para busca.
Agregações. Counters em Redis, tabela summary em columnar (ClickHouse), OLAP cube. Pré-calcula agregações comuns.
Time-series projections. InfluxDB, Prometheus. Eventos viram pontos em time-series; queries retornam séries temporais eficientemente.
Graph projections. Neo4j para relacionamentos. Útil para social, recomendação, navegação por relações.
Sistema com múltiplas projeções ganha cada uma otimizada para uso específico. Custos: cada projeção precisa ser construída, mantida, e sincronizada. Mais projeções = mais infra.
Quando CQRS vale — e quando não
A decisão de adotar CQRS é frequentemente errada, em ambas direções. Critérios para decidir.
Vale quando:
- Read scale >> write scale (10× ou mais).
- Modelos de leitura naturalmente diferentes do modelo de escrita (busca, dashboard, relatório).
- Equipe tem experiência distribuída (eventos, eventual consistency).
- Sistema já está em escala e performance é gargalo identificado.
- Auditoria nativa é requisito (nesse caso, event sourcing).
Não vale quando:
- Sistema jovem, padrões de uso ainda aprendendo. CQRS prematuramente complica evolução.
- Equipe não tem experiência com eventual consistency. Bugs sutis aparecem.
- Performance ainda não é problema. Vertical scaling de banco é mais simples e mais barato.
- Modelo de escrita e leitura são similares. Separar não traz valor.
- Sistema crítico de transações financeiras simples (CRUD bancário). Strong consistency em modelo único basta; CQRS adicionaria risco.
A regra prática: CQRS é remediação de problema de escala, não pré-requisito de boa arquitetura. Sistema bem desenhado em modelo único pode adotar CQRS depois quando dor real aparecer. Sistema que adota CQRS preventivamente carrega complexidade sem retorno.
Caminho de adoção progressiva
Para times que decidem adotar CQRS, há caminho progressivo que minimiza risco.
Passo 1. Separar CommandHandlers e QueryHandlers no código (nível 1). Mesma stack; sem mudança de infra. Apenas organização. Time aprende a pensar em separação.
Passo 2. Adicionar read replicas para queries pesadas (nível 2). Conceito 05 cobre. Aceita lag pequeno; testa read-your-writes onde necessário.
Passo 3. Para queries que pedem otimização específica (busca, dashboard), adicionar tabelas/projeções denormalizadas (nível 3). Sincronização via app code ou triggers.
Passo 4. Quando uma projeção pede ferramenta diferente (Elasticsearch para busca, Redis para counters), externalizar (nível 4). Pipeline de eventos via outbox ou CDC. Eventual consistency assumida.
Passo 5. Considerar event sourcing apenas se a maioria das projeções já vive externamente e auditoria/replay são requisitos do domínio. Decisão grande.
Times que pulam passos frequentemente tropeçam — adotar event sourcing direto sem experiência com eventual consistency é receita para incidentes prolongados.
CQRS em três stacks
Implementação varia por stack.
// MediatR é a biblioteca canônica para CQRS leve em .NET
// commands e queries são tipos distintos com handlers próprios
// command (modifica estado)
public record CriarPedidoCommand(Guid ClienteId, List<ItemDto> Itens) : IRequest<Guid>;
public class CriarPedidoHandler : IRequestHandler<CriarPedidoCommand, Guid>
{
private readonly IPedidoRepository _repo;
private readonly IMediator _mediator;
public async Task<Guid> Handle(CriarPedidoCommand cmd, CancellationToken ct)
{
var pedido = new Pedido(cmd.ClienteId, cmd.Itens); // domínio rico
await _repo.SalvarAsync(pedido, ct);
// emite evento para projeções atualizarem
await _mediator.Publish(new PedidoCriado(pedido.Id, pedido.ClienteId), ct);
return pedido.Id;
}
}
// query (não modifica estado)
public record ListarPedidosQuery(Guid ClienteId, int Page) : IRequest<PagedResult<PedidoSummary>>;
public class ListarPedidosHandler : IRequestHandler<ListarPedidosQuery, PagedResult<PedidoSummary>>
{
private readonly IDbConnection _readDb; // pode ser replica
public async Task<PagedResult<PedidoSummary>> Handle(ListarPedidosQuery q, CancellationToken ct)
{
// SQL otimizado direto, sem modelo de domínio
var pedidos = await _readDb.QueryAsync<PedidoSummary>(@"
SELECT id, cliente_nome, total, status
FROM pedidos_view
WHERE cliente_id = @ClienteId
ORDER BY created_at DESC
LIMIT 20 OFFSET @Offset",
new { q.ClienteId, Offset = q.Page * 20 });
return new PagedResult<PedidoSummary>(pedidos.ToList(), q.Page);
}
}
// projeção atualiza pedidos_view via event handler
public class PedidoCriadoProjector : INotificationHandler<PedidoCriado>
{
public async Task Handle(PedidoCriado evt, CancellationToken ct)
{
// atualiza tabela denormalizada pedidos_view
await _conn.ExecuteAsync(@"
INSERT INTO pedidos_view (id, cliente_id, cliente_nome, total, status)
SELECT p.id, p.cliente_id, c.nome, p.total, p.status
FROM pedidos p JOIN clientes c ON c.id = p.cliente_id
WHERE p.id = @Id", new { Id = evt.PedidoId });
}
}
MediatR formaliza separação Command/Query no código, sem precisar de event sourcing. Cada handler tem responsabilidade clara; queries podem ir direto a SQL otimizado, commands mantêm domínio rico.
from typing import Protocol
from dataclasses import dataclass
# command
@dataclass
class CriarPedidoCommand:
cliente_id: UUID
itens: list[Item]
# command handler
class CriarPedidoHandler:
def __init__(self, repo: PedidoRepository, events: EventBus):
self.repo = repo
self.events = events
async def handle(self, cmd: CriarPedidoCommand) -> UUID:
pedido = Pedido(cliente_id=cmd.cliente_id, itens=cmd.itens)
await self.repo.salvar(pedido)
await self.events.publish(PedidoCriado(pedido.id, pedido.cliente_id))
return pedido.id
# query handler — SQL direto, sem ORM rico
class ListarPedidosQuery:
def __init__(self, read_db: AsyncSession):
self.read_db = read_db
async def execute(self, cliente_id: UUID, page: int = 0) -> list[PedidoSummary]:
result = await self.read_db.execute(
text("""
SELECT id, cliente_nome, total, status
FROM pedidos_view
WHERE cliente_id = :cliente_id
ORDER BY created_at DESC
LIMIT 20 OFFSET :offset
"""),
{"cliente_id": cliente_id, "offset": page * 20}
)
return [PedidoSummary(**row) for row in result]
# FastAPI endpoints invocam handlers
@app.post("/pedidos", status_code=201)
async def criar_pedido(
cmd: CriarPedidoCommand,
handler: CriarPedidoHandler = Depends(get_command_handler),
):
pedido_id = await handler.handle(cmd)
return {"id": pedido_id}
@app.get("/pedidos")
async def listar_pedidos(
cliente_id: UUID,
page: int = 0,
query: ListarPedidosQuery = Depends(get_query_handler),
):
return await query.execute(cliente_id, page)
Python sem framework formal de CQRS — separação é convenção. Pattern: handler classes para commands com domínio rico; query handlers com SQL direto. Eventbus para notificar projeções.
// pacote app/commands
package commands
type CriarPedido struct {
ClienteID uuid.UUID
Itens []Item
}
type CriarPedidoHandler struct {
repo PedidoRepository
events EventBus
}
func (h *CriarPedidoHandler) Handle(ctx context.Context, cmd CriarPedido) (uuid.UUID, error) {
pedido, err := domain.NewPedido(cmd.ClienteID, cmd.Itens)
if err != nil { return uuid.Nil, err }
if err := h.repo.Salvar(ctx, pedido); err != nil {
return uuid.Nil, err
}
h.events.Publish(ctx, events.PedidoCriado{
PedidoID: pedido.ID,
ClienteID: pedido.ClienteID,
})
return pedido.ID, nil
}
// pacote app/queries
package queries
type ListarPedidos struct {
ClienteID uuid.UUID
Page int
}
type PedidoSummary struct {
ID uuid.UUID
ClienteNome string
Total decimal.Decimal
Status string
}
type ListarPedidosHandler struct {
db *pgxpool.Pool // pode ser read replica
}
func (h *ListarPedidosHandler) Handle(ctx context.Context, q ListarPedidos) ([]PedidoSummary, error) {
rows, err := h.db.Query(ctx, `
SELECT id, cliente_nome, total, status
FROM pedidos_view
WHERE cliente_id = $1
ORDER BY created_at DESC
LIMIT 20 OFFSET $2
`, q.ClienteID, q.Page*20)
if err != nil { return nil, err }
var summaries []PedidoSummary
for rows.Next() {
var s PedidoSummary
rows.Scan(&s.ID, &s.ClienteNome, &s.Total, &s.Status)
summaries = append(summaries, s)
}
return summaries, nil
}
// projector — escuta eventos e atualiza pedidos_view
package projections
type PedidoSummaryProjector struct {
db *pgxpool.Pool
}
func (p *PedidoSummaryProjector) On(ctx context.Context, evt events.PedidoCriado) error {
_, err := p.db.Exec(ctx, `
INSERT INTO pedidos_view (id, cliente_id, cliente_nome, total, status)
SELECT p.id, p.cliente_id, c.nome, p.total, p.status
FROM pedidos p JOIN clientes c ON c.id = p.cliente_id
WHERE p.id = $1
`, evt.PedidoID)
return err
}
Go organiza CQRS por package: commands, queries, projections como diretórios separados. Sem framework de mediator — composição manual. Vence em explicitness.
Event sourcing — quando vale
Event sourcing é o irmão pesado de CQRS. Em vez de armazenar estado, armazena sequência de eventos. Estado é derivado por replay.
Vale quando:
Auditoria nativa. Sistemas financeiros, médicos, legais. Cada mudança é evento imutável; histórico completo automaticamente.
Replay para corrigir bugs. Em sistema com event log, você pode mudar lógica de projeção e reconstruir read-models do zero, aplicando nova lógica a eventos antigos. Útil para "esqueci de calcular X em pedidos antigos".
Múltiplas projeções da mesma fonte. Eventos viram source of truth; cada projeção é visão. Adicionar nova projeção (nova feature) não exige migration de dados — apenas replay.
Domínio naturalmente event-driven. Pagamentos, filas de mensageria, workflows complexos. Eventos refletem semântica do domínio.
Não vale quando:
Domínio é primariamente CRUD. Sistemas de cadastro simples; estado atual basta.
Equipe sem experiência. Event sourcing tem armadilhas únicas: event versioning (eventos publicados em formato antigo precisam sobreviver mudanças de schema), snapshots (estado reconstruído por replay precisa de otimização para coleções grandes), aggregate design (eventos por agregado, não por entidade individual). Adotar sem experiência é tropeço garantido.
Tooling limitado. Event sourcing em Postgres puro funciona mas não é trivial. EventStoreDB é solução dedicada; AxonFramework em Java; aplicações .NET podem usar Marten ou EventFlow. Em algumas linguagens, falta tooling maduro.
Anti-padrões frequentes
CQRS preventivo. Adotar CQRS sem evidência de problema de escala. Complexidade sem retorno. Defesa: começar com modelo único; CQRS quando dor real aparecer.
CQRS sem aceitar eventual consistency. Adoção de CQRS, mas equipe assume reads imediatamente consistentes com writes. Bugs sutis em read-your-writes. Defesa: educar equipe sobre eventual consistency antes de adotar; aplicar mitigations conhecidas.
Event sourcing direto. Pular para event sourcing pleno sem passar pelos níveis anteriores. Complexidade enorme; bugs em versioning, snapshots, projeção. Defesa: caminho progressivo (níveis 1-4 antes de 5).
Esquecer event versioning. Em event sourcing, schema dos eventos vive para sempre. Mudar campo "valor" de int para decimal quebra eventos antigos. Defesa: versionamento explícito desde início; upcasters; nunca mudança breaking.
Projeções sem rebuilds testados. Sistema com event log; projeções nunca foram reconstruídas do zero. Quando precisa (bug, schema change), descobre que rebuild quebra. Defesa: testar replay regularmente em staging.
Mistura de patterns. Sistema híbrido onde algumas partes são CQRS pleno e outras CRUD tradicional, sem articular fronteiras. Times confundem. Defesa: articular bounded contexts (DDD); cada contexto escolhe seu padrão.
Read-your-writes quebrado em CQRS pleno. Cliente cria pedido (write commitado em write-model); cliente é redirecionado para listagem (read de read-model); pedido novo não aparece (lag de sincronização do projector). Cliente confuso — "não foi salvo?"; envia novamente; agora tem duplicata. Em CQRS pleno, esse é problema real e recorrente. Defesa: para fluxos onde read-your-writes importa, ler diretamente do write-model (single-shard query) por janela após escrita; ou retornar dado sintético do command response (otimista).
Antes de adotar CQRS, articule cinco coisas. "Read scale realmente domina write scale 10× ou mais?". "Tenho pelo menos 1 query que modelo único atende mal?". "Equipe entende eventual consistency?". "Vale o investimento operacional (mais bancos, sincronização, monitoring)?". "Posso adotar progressivamente?". Se 4-5 sims claros, vale. Se ambíguo, ainda é cedo. CQRS é ferramenta poderosa para problemas certos — e overkill para a maioria.
Resumindo o módulo — escala como propriedade
Os doze conceitos do módulo cobrem escalabilidade em camadas. Decisão fundamental (vertical vs horizontal, conceito 01); propriedade fundadora (stateless, conceito 02); ferramentas para distribuir (load balancing, consistent hashing, conceitos 03-04); banco distribuído (replicação, sharding, conceitos 05-06); limites teóricos (CAP/ PACELC, conceito 07); operação dinâmica (autoscaling, backpressure, conceitos 08-09); arquitetura para múltiplos clientes (multi-tenancy, conceito 10); distribuição geográfica (multi-region, conceito 11); padrão avançado para escalar leitura (CQRS, este conceito).
Em sistema bem desenhado, cada camada contribui. Aplicação stateless escalonada por load balancer; banco com replication para reads; sharding para writes quando necessário; consistent hashing em caches; autoscaling com behavior assimétrico; backpressure protegendo de overload; multi-tenancy para múltiplos clientes; multi-region para SLA acima de 99.99%; CQRS onde leitura domina.
Em sistema mal desenhado, qualquer camada ausente vira o gargalo. Sistema sem stateless não escalona horizontalmente; sem load balancing, sobrecarga distribui mal; sem replication, banco satura; sem CAP entendido, decisões erradas sob particionamento. A maturidade arquitetural é enxergar todas essas dimensões e articular qual precisa neste sistema, neste momento.
Por que importa para a sua carreira
CQRS é vocabulário que aparece em entrevistas avançadas e em sistemas com escala já estabelecida. Quem articula CQRS em pragmatismo (não como hype) se distingue. Em entrevistas, "como você escalonaria leitura sem sacrificar consistência forte de writes?" é convite para CQRS — a resposta forte cita níveis progressivos, eventual consistency, event sourcing como opção avançada. Em revisão de proposta, identificar CQRS preventivo (sem necessidade) é serviço crítico (evita complexidade desnecessária). Em pos-mortem de "read-your-writes quebrou em sistema CQRS", diagnóstico estrutural é trabalho de senior. Em arquitetura de sistemas grandes, articular bounded contexts e qual usa CQRS, qual não, é vocabulário maduro.
Como praticar
- Implementar CQRS leve em projeto seu. Pegue projeto existente; identifique 2-3 commands e 2-3 queries; refatore para handlers separados (sem mudar banco). Compare clareza de código antes/depois. Esse exercício é low-risk, e revela que CQRS leve é frequentemente vitória pura sem custos.
- Read replica para queries. Em projeto com banco replicado, configure roteamento explícito: queries para replica, commands para primary. Mensure: reduziu carga no primary? Quanto lag entre commit e read? Identifique queries que falharam com lag e articule mitigations (read-your-writes via primary).
- Modelar event sourcing simples. Sem implementar em produção, modele um domínio seu (carrinho, checkout, fluxo de aprovação) em event sourcing. Liste eventos. Articule como cada projeção (saldo, listagem, dashboard) seria derivada. Identifique versioning challenges (o que acontece se mudar campo X depois). Esse exercício mental é o mais barato para entender se vale para seu contexto.
Referências para aprofundar
- artigo CQRS Documents — Greg Young (2010).
- artigo CQRS — Martin Fowler (martinfowler.com, 2011).
- livro Implementing Domain-Driven Design — Vaughn Vernon (Addison-Wesley, 2013).
- livro Microservices Patterns — Chris Richardson (Manning, 2018).
- livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017).
- livro Versioning in an Event Sourced System — Greg Young (Leanpub, 2017).
- livro Practical Microservices — Ethan Garofolo (Pragmatic Bookshelf, 2020).
- artigo Event Sourcing — Martin Fowler (martinfowler.com, 2005).
- artigo The Many Meanings of Event-Driven Architecture — Martin Fowler (2017).
- docs MediatR.
- docs EventStoreDB.
- vídeo CQRS and Event Sourcing — Greg Young (várias palestras 2014–2020).