CQRS e Event Sourcing para SaaS Moderno

CQRS e Event Sourcing são os padrões de arquitetura por trás dos produtos SaaS mais escaláveis. Um guia prático para CTOs de startups.

Cover Image for CQRS e Event Sourcing para SaaS Moderno

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:

  1. OrderCreated { orderId: "abc", customerId: "xyz", items: [...] }
  2. ItemAdded { orderId: "abc", productId: "p1", quantity: 2 }
  3. PaymentReceived { orderId: "abc", amount: 149.99 }
  4. 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:

  1. Cliente envia um comandoShipOrder { orderId: "abc" }
  2. Command handler carrega o agregado reproduzindo seus eventos
  3. Regras de negócio são validadas — O pedido foi pago? Há estoque disponível?
  4. Novos eventos são emitidosOrderShipped { orderId: "abc", ... }
  5. Eventos são persistidos no event store
  6. 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 comando SendShippingNotification
  • PaymentFailed → disparar comando CancelOrder
  • SubscriptionExpired → disparar comando SuspendTenantAccess

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. OrderShipped significa 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.