CQRS e Event Sourcing¶
Dois padrões que costumam aparecer juntos em sistemas orientados a eventos (Event-Driven Architecture) e modelados com DDD: CQRS separa leitura de escrita; Event Sourcing guarda os eventos em vez do estado atual.
CQRS¶
Definição: CQRS (Command Query Responsibility Segregation)
Separa as operações que alteram o estado (commands) das que apenas consultam (queries), com modelos distintos para cada lado: o de escrita protege regras e consistência; o de leitura é otimizado para consultar.
| Command (escrita) | Query (leitura) | |
|---|---|---|
| Intenção | Mudar o estado: criar, atualizar, cancelar | Consultar dados |
| Retorno | Não devolve dados de negócio (no máximo um identificador) | Devolve DTOs / view models |
| Foco | Validar regras e invariantes | Desempenho e formato da resposta |
| Armazenamento | Banco transacional | Pode ser outro banco (réplica, NoSQL, índice de busca) |
public record CriarPedidoCommand(Long clienteId, List<ItemDTO> itens) {}
public record BuscarPedidosQuery(Long clienteId) {}
public class CriarPedidoHandler { // um handler por comando
public void handle(CriarPedidoCommand cmd) {
// valida, persiste e publica o evento PedidoCriado
}
}
flowchart LR
C["Cliente"] -->|Command| H["Handler de escrita"]
H --> W[("Banco de escrita")]
H -->|evento| P["Projetor"]
P --> R[("Banco de leitura")]
C -->|Query| Q["Handler de leitura"]
Q --> R
- Modelos separados: o de escrita pode ter regras ricas; o de leitura usa estruturas desnormalizadas e DTOs prontos para a tela.
- Consistência eventual: depois de um command, a leitura pode levar um instante para refletir a mudança — é o comportamento esperado. Comunique isso ao usuário e evite depender de leitura imediata.
- Escalabilidade: escrita e leitura escalam de forma independente (várias réplicas de leitura, caches, índices).
- Projeções: modelos de leitura montados a partir de eventos (
PedidoCriadoEvent), atualizados de forma assíncrona; podem existir várias, uma por necessidade de consulta. - Eventos de domínio: algo que aconteceu no negócio, publicado após o command para atualizar projeções e notificar outros serviços (Kafka, RabbitMQ).
Quando NÃO usar
CQRS adiciona complexidade (dois modelos, projeções, consistência eventual). Se o CRUD simples resolve, não use. Comece pequeno, avalie o custo, monitore o atraso das projeções e documente o fluxo. É útil quando leitura e escrita têm necessidades muito diferentes (carga, forma dos dados, escalabilidade).
Event Sourcing¶
Definição: Event Sourcing
Em vez de gravar só o estado atual, grava-se a sequência de eventos que levou até ele. O estado é obtido reaplicando os eventos em ordem (replay). O histórico é a fonte da verdade.
public record PedidoCriadoEvent(String pedidoId, String clienteId,
Instant ocorridoEm, int versaoSchema) {} // evento imutável
eventStore.append("pedido-123", new PedidoCriadoEvent(...));
PedidoAggregate p = new PedidoAggregate();
for (Evento e : eventStore.carregar("pedido-123")) {
p.aplicar(e); // reconstrói o estado
}
| Conceito | Detalhe |
|---|---|
| Event store | Repositório append-only (só acrescenta), com ordem e durabilidade; consulta por agregado e por período |
| Eventos imutáveis | Fatos no passado ("PedidoConfirmado"), nunca alterados; carregam dados e timestamp; têm versão de schema |
| Replay | Reconstrói o estado atual (ou de um instante passado) reaplicando eventos; permite simulações e correções |
| Agregado | Valida regras e gera os eventos; o estado é derivado deles (ver DDD) |
| Snapshots | "Fotos" do estado em um ponto, para não reaplicar milhares de eventos: carrega-se o snapshot e só os eventos posteriores |
| Versionamento de eventos | Eventos antigos continuam existindo: use upcasters (conversão entre versões) e múltiplas versões na leitura |
| Auditoria | Registro completo de quem, o quê e quando, útil para conformidade (ex.: LGPD) |
Consistência: o Event Sourcing costuma ser combinado com CQRS: o lado de escrita grava eventos; projeções assíncronas montam os modelos de leitura (consistência eventual). Exemplo de fluxo: pedido criado → confirmado → cancelado; o estado atual é a soma desses eventos.
Cuidados: exige disciplina com o esquema dos eventos, ferramentas para replay e projeções e uma equipe que domine o modelo; para dados com pouco valor histórico, o custo não compensa.
Multi-tenancy (arquitetura de dados)¶
Um tenant é um cliente que usa a mesma aplicação, compartilhando a infraestrutura mas com dados e configuração isolados.
| Modelo | Isolamento | Custo | Quando |
|---|---|---|---|
| Banco por cliente | Máximo (físico) | Alto | Clientes grandes, exigências regulatórias |
| Schema por cliente | Bom (lógico, no mesmo banco) | Médio | Clientes médios |
Coluna tenant_id |
Menor (lógico por linha) | Baixo | Muitos clientes pequenos |
@Entity
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = String.class))
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class Pedido { @Column(name = "tenant_id") private String tenantId; /* ... */ }
- Resolução do tenant na requisição: subdomínio (
cliente1.app.com), cabeçalho (X-Tenant-ID), claim do JWT ou parâmetro; guarde em um contexto (ThreadLocal) e limpe-o ao final. - Filtros de dados: aplique-os automaticamente (filtro do Hibernate,
@Where) para nunca esquecer otenant_id— um vazamento entre clientes (data leak) é grave. - Segurança: valide o tenant no serviço, use RBAC e faça auditoria.
- Escala: comece com um modelo simples e permita migrar para um mais isolado quando um cliente crescer.