Por Lucas Gertel, CTO da Meld
Todo produto SaaS começa simples. Algumas tabelas no banco, uns endpoints CRUD, um frontend em React. Funciona lindamente — até parar de funcionar. No momento em que você precisa de trilhas de auditoria, regras de negócio complexas, isolamento multi-tenant ou projeções em tempo real em diferentes contextos de leitura, o modelo "só atualiza a linha" começa a rachar.
Passei 20 anos construindo sistemas enterprise — de plataformas de grande escala na Avenue Code a formar a próxima geração de arquitetos na Software Architect Academy no Brasil. O padrão ao qual sempre volto para produtos SaaS sérios é a combinação de CQRS e Event Sourcing. Não porque estão na moda. Porque resolvem problemas reais que todo SaaS em crescimento eventualmente encontra.
Este é um guia prático. Sem abstrações acadêmicas. Apenas os padrões, trade-offs e decisões de implementação que importam quando você está construindo um produto que precisa escalar.
O Que CQRS Realmente É
Command Query Responsibility Segregation (Segregação de Responsabilidade entre Comandos e Consultas), descrito por Martin Fowler, é uma ideia simples com implicações profundas: separe seu modelo de escrita do seu modelo de leitura.
Em uma arquitetura tradicional, o mesmo modelo de dados serve a ambos os propósitos. Sua tabela users é onde você grava dados de novos usuários e onde lê para exibição. Seu ORM mapeia uma classe para uma tabela, e essa classe trata comandos ("crie este usuário") e consultas ("mostre todos os usuários ativos") de forma idêntica.
CQRS diz: pare de fazer isso. Em vez disso, construa dois caminhos distintos:
- Lado de comando: Trata escritas. Valida regras de negócio. Garante consistência. Emite eventos quando o estado muda.
- Lado de consulta: Trata leituras. Otimizado para as views específicas que sua UI precisa. Desnormalizado, rápido e eventualmente consistente com o lado de comando.
Por Que Isso Importa
Os lados de leitura e escrita da maioria das aplicações têm requisitos fundamentalmente diferentes:
- Escritas precisam de consistência forte, validação e garantias transacionais
- Leituras precisam de velocidade, flexibilidade e a capacidade de servir muitos formatos diferentes de view a partir dos mesmos dados subjacentes
Quando você força ambos pelo mesmo modelo, compromete os dois. Seu modelo de escrita fica poluído com campos calculados e lógica de join para exibição. Seu modelo de leitura fica lento por regras de normalização que existem para integridade de escrita.
CQRS permite que cada lado otimize independentemente. Seu modelo de comando pode ser um modelo de domínio rico com regras de negócio complexas. Seu modelo de leitura pode ser projeções planas e desnormalizadas — uma por view se necessário — servidas do Redis, Elasticsearch ou materialized views pré-computadas.
O Que Event Sourcing Realmente É
Event Sourcing inverte o modelo tradicional de persistência. Em vez de armazenar o estado atual de uma entidade, você armazena a sequência de eventos que produziu aquele estado.
Um sistema tradicional armazena: Order { status: "shipped", total: 149.99, items: [...] }
Um sistema event-sourced armazena:
OrderCreated { orderId: "abc", customerId: "xyz", items: [...] }ItemAdded { orderId: "abc", productId: "p1", quantity: 2 }PaymentReceived { orderId: "abc", amount: 149.99 }OrderShipped { orderId: "abc", trackingNumber: "1Z999..." }
O estado atual é derivado pela reprodução desses eventos. O log de eventos é a fonte da verdade. O "estado atual" é apenas uma projeção conveniente.
Por Que Isso Importa
- Trilha de auditoria completa — Você não sabe apenas o estado atual. Sabe cada estado pelo qual a entidade já passou e exatamente por que fez a transição.
- Consultas temporais — Como estava esse pedido na terça passada? Reproduza os eventos até terça. Pronto.
- Investigação de bugs — Quando algo dá errado, você pode reproduzir a sequência exata de eventos que levou ao estado problemático.
- Novas projeções sem migration — Precisa de um novo relatório ou dashboard? Crie uma nova projeção e reproduza os eventos históricos por ela. Sem necessidade de migração de dados.
Por Que CQRS e Event Sourcing Funcionam Juntos
Esses padrões são independentes — você pode usar CQRS sem Event Sourcing e vice-versa — mas são complementos naturais.
Eventos são a ponte entre os lados de comando e consulta. Quando o lado de comando processa um comando e o valida contra regras de negócio, ele emite eventos de domínio. Esses eventos são persistidos no event store (Event Sourcing) e simultaneamente projetados em modelos de leitura (CQRS).
O fluxo funciona assim:
- Cliente envia um comando →
ShipOrder { orderId: "abc" } - Command handler carrega o agregado reproduzindo seus eventos
- Regras de negócio são validadas — O pedido foi pago? Há estoque disponível?
- Novos eventos são emitidos →
OrderShipped { orderId: "abc", ... } - Eventos são persistidos no event store
- Projeções consomem os eventos e atualizam modelos de leitura
O event store é a única fonte da verdade. Modelos de leitura são projeções descartáveis e reconstruíveis, otimizadas para padrões de consulta específicos.
Quando Usar CQRS + Event Sourcing
Esses padrões adicionam complexidade. Não são apropriados para tudo. Veja quando compensam:
Bom Fit
- Sistemas de alta escrita onde cargas de leitura e escrita precisam escalar independentemente
- Requisitos de auditoria e compliance — saúde, finanças, aviação, jurídico
- Lógica de domínio complexa onde regras de negócio evoluem frequentemente e você precisa entender o histórico de cada decisão
- SaaS multi-tenant onde tenants precisam de fluxos de dados isolados e auditáveis
- Integrações orientadas a eventos onde sistemas downstream reagem a eventos de domínio
- Sistemas que requerem consultas temporais — "Qual era o estado de X no momento T?"
Fit Ruim
- Aplicações CRUD simples — Se seu domínio é apenas "salve este formulário e exiba depois," CQRS+ES é over-engineering
- MVPs iniciais com domínios pouco claros — Você precisa entender suas fronteiras de domínio antes de fazer event sourcing nelas. Event sourcing prematuro cria abstrações prematuras
- Sistemas com muita leitura e pouca escrita — Um site de conteúdo ou catálogo que raramente muda não se beneficia de otimização do lado de escrita
- Times sem experiência com arquitetura orientada a eventos — A curva de aprendizado é real. Forçar CQRS+ES em um time que nunca trabalhou com consistência eventual gera sofrimento
Implementação Prática
Vou percorrer os componentes-chave e decisões de tecnologia.
O Event Store
Na forma mais simples, um event store é uma tabela no PostgreSQL:
events ( id UUID PRIMARY KEY, aggregate_id UUID NOT NULL, aggregate_type VARCHAR NOT NULL, event_type VARCHAR NOT NULL, event_data JSONB NOT NULL, metadata JSONB, version INTEGER NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(aggregate_id, version) )
A constraint UNIQUE(aggregate_id, version) é crítica — ela fornece controle de concorrência otimista. Se dois comandos tentam anexar eventos ao mesmo agregado simultaneamente, um falhará na verificação de unicidade e pode ser retentado.
Para a maioria dos produtos SaaS, PostgreSQL é suficiente como event store. Você não precisa de EventStoreDB ou Kafka desde o dia um. Comece com Postgres, adicione infraestrutura especializada quando seu volume de eventos exigir.
Command Handlers
Um command handler recebe um comando, carrega o agregado relevante, executa lógica de negócio e persiste os eventos resultantes:
async function handleShipOrder(command: ShipOrder): Promise<void> {
const events = await eventStore.getEvents(command.orderId);
const order = Order.rehydrate(events);
// Validação de regras de negócio
order.ship(command.trackingNumber);
// Persistir novos eventos
await eventStore.append(
command.orderId,
order.uncommittedEvents,
order.version
);
}
O agregado em si é um objeto de domínio puro. Sem dependências de banco. Sem ORM. Apenas lógica de negócio que aceita comandos e emite eventos.
Projeções e Modelos de Leitura
Projeções são event handlers que constroem views otimizadas para leitura. Elas se inscrevem no stream de eventos e atualizam modelos de leitura desnormalizados:
function orderSummaryProjection(event: DomainEvent): void {
switch (event.type) {
case 'OrderCreated':
readDb.insert('order_summaries', {
orderId: event.orderId,
customerName: event.customerName,
status: 'created',
total: 0
});
break;
case 'OrderShipped':
readDb.update('order_summaries',
{ orderId: event.orderId },
{ status: 'shipped', shippedAt: event.timestamp }
);
break;
}
}
Modelos de leitura podem viver no PostgreSQL, Redis, Elasticsearch — o que melhor servir o padrão de consulta. Eles são descartáveis. Se uma projeção tem um bug, corrija o código, descarte o modelo de leitura e reproduza todos os eventos pela projeção corrigida.
Sagas e Process Managers
Sistemas reais têm workflows que abrangem múltiplos agregados. Uma saga (ou process manager) coordena esses fluxos ouvindo eventos e despachando comandos:
OrderShipped→ disparar comandoSendShippingNotificationPaymentFailed→ disparar comandoCancelOrderSubscriptionExpired→ disparar comandoSuspendTenantAccess
As próprias sagas são event-sourced, dando visibilidade total sobre o estado de cada processo de longa duração no seu sistema.
Consistência Eventual: O Trade-Off Que Você Precisa Aceitar
CQRS com modelos de leitura separados significa que leituras são eventualmente consistentes. Quando um comando é bem-sucedido e eventos são persistidos, os modelos de leitura podem levar de milissegundos a segundos para atualizar.
Para a maioria das aplicações SaaS, isso é perfeitamente aceitável. Usuários não percebem um delay de 200ms entre clicar em "enviar" e ver os dados atualizados. Mas você precisa projetar para isso:
- Retorne resultados de comando diretamente — Não faça a UI ficar consultando o modelo de leitura após um comando. Retorne sucesso/falha do command handler.
- Use atualizações otimistas na UI — Atualize a UI imediatamente ao sucesso do comando, depois reconcilie quando o modelo de leitura alcançar.
- Projete projeções idempotentes — Eventos podem ser entregues mais de uma vez. Suas projeções devem lidar com duplicatas graciosamente.
A Conexão com DDD
CQRS e Event Sourcing são extensões naturais do Domain-Driven Design. As conexões são profundas:
- Agregados são as fronteiras de consistência do seu modelo de comando. Cada agregado é uma entidade event-sourced com seu próprio stream de eventos.
- Eventos de domínio são cidadãos de primeira classe, não algo pensado depois. São o contrato de integração entre bounded contexts.
- Bounded contexts mapeiam para serviços deployáveis independentemente, cada um com seu próprio event store e modelos de leitura.
- Linguagem ubíqua é codificada diretamente nos tipos de evento.
OrderShippedsignifica a mesma coisa para desenvolvedores, especialistas de domínio e a codebase.
Se você ainda não está pensando em termos de DDD, comece por aí antes de adotar Event Sourcing. Entender suas fronteiras de domínio é um pré-requisito, não um aprimoramento opcional.
Quando Usamos na Meld
Não aplicamos CQRS+ES em todos os projetos. Para um MVP SaaS direto com semânticas CRUD claras, uma arquitetura tradicional bem estruturada é mais rápida de construir e mais fácil de manter.
Mas quando um cliente chega com requisitos de compliance, lógica de domínio complexa, necessidades de trilha de auditoria ou isolamento de dados multi-tenant, CQRS e Event Sourcing são nossa recomendação padrão. O investimento inicial se paga em meses conforme o sistema evolui e novos requisitos surgem que seriam um pesadelo com estado mutável.
O insight-chave de duas décadas de arquitetura enterprise: escolha seus padrões baseado na complexidade real do seu domínio, não na simplicidade percebida. Um domínio que parece simples hoje frequentemente revela complexidade oculta quando usuários reais e dinheiro real estão envolvidos. Event Sourcing dá a flexibilidade para lidar com essa complexidade sem reescrever sua camada de persistência.
Construa sua fundação para os problemas que você realmente vai ter. Esse é o trabalho do arquiteto.
