Como usar o padrão read model para projeções customizadas por cliente
1. Fundamentos do padrão read model em sistemas orientados a eventos
1.1. Definição e propósito do read model: separação entre escrita e leitura
O padrão read model estabelece uma separação clara entre o modelo de escrita (command model) e o modelo de leitura (read model). Enquanto o command model é otimizado para validação de regras de negócio e persistência de eventos, o read model é construído especificamente para atender consultas de forma eficiente. Essa separação permite que cada lado evolua independentemente, com esquemas e tecnologias de armazenamento diferentes.
1.2. Diferença entre event store, materialized views e projeções customizadas
- Event store: repositório imutável de todos os eventos ocorridos no sistema. Serve como fonte da verdade.
- Materialized views: visões pré-computadas a partir dos eventos, geralmente padronizadas para todos os clientes.
- Projeções customizadas: visões especializadas que aplicam transformações, filtros e agregações específicas por cliente.
1.3. Relação com o padrão CQRS e event sourcing
O read model é um dos pilares do CQRS (Command Query Responsibility Segregation). Quando combinado com event sourcing, os read models são reconstruídos a partir do stream de eventos, garantindo rastreabilidade total. As projeções customizadas por cliente surgem naturalmente nesse contexto, pois cada inquilino pode ter suas próprias regras de transformação.
2. Desafios de projeções customizadas por cliente
2.1. Necessidade de visões de dados específicas por inquilino ou usuário
Em sistemas multi-tenant, cada cliente pode exigir formatos de dados distintos: um cliente quer relatórios financeiros consolidados, outro quer histórico de interações detalhado, e um terceiro precisa de dashboards em tempo real. O read model precisa acomodar essas variações sem comprometer a performance.
2.2. Complexidade de manutenção quando cada cliente exige formatos diferentes
Manter dezenas ou centenas de projeções diferentes pode se tornar um pesadelo operacional. Cada nova projeção introduz lógica de transformação, armazenamento e testes específicos. Sem governança adequada, o sistema rapidamente se torna frágil.
2.3. Trade-offs entre armazenamento dedicado vs consulta dinâmica
- Armazenamento dedicado: projeções pré-computadas por cliente — consultas rápidas, mas maior custo de armazenamento.
- Consulta dinâmica: computa a projeção sob demanda a partir dos eventos — menor custo de armazenamento, mas maior latência e carga computacional.
A escolha depende do volume de dados, frequência de consulta e requisitos de latência de cada cliente.
3. Estrutura de um read model para múltiplos clientes
3.1. Componentes essenciais: event handlers, processadores de projeção e armazenamento de saída
- Event handlers: escutam o event store e disparam processamento assíncrono.
- Processadores de projeção: aplicam lógica de transformação específica por cliente.
- Armazenamento de saída: banco de dados otimizado para consultas (relacional, NoSQL ou colunar).
3.2. Estratégias de chaveamento: client_id, tenant_id ou contexto de usuário
Cada evento deve ser roteado para o processador correto usando um identificador de cliente. As estratégias comuns incluem:
- Partition key baseada em
tenant_id - Fila de eventos separada por cliente
- Filtro no event handler que verifica metadados do evento
3.3. Contratos de projeção: definindo o esquema por cliente sem acoplamento
Cada projeção customizada deve ter um contrato explícito (schema versionado) que define quais campos serão projetados. Esse contrato é registrado em um repositório de projeções, permitindo que novos clientes sejam adicionados sem modificar o código existente.
4. Implementação de projeções customizadas com event handlers
4.1. Exemplo de handler que filtra e transforma eventos por cliente
// Event handler para projeção customizada do Cliente A
function handleOrderPlacedForClientA(event, context) {
const { clientId, orderId, items, total } = event.data;
// Filtro: somente pedidos acima de R$ 1000
if (total < 1000) return;
// Transformação: calcular comissão específica do cliente
const commission = total * 0.05;
// Projeção: armazenar visão customizada
const projection = {
clientId,
orderId,
amount: total,
commissionAmount: commission,
processedAt: new Date().toISOString()
};
return projection;
}
4.2. Uso de funções de projeção parametrizadas por regras de negócio do cliente
// Função de projeção parametrizada
function createProjectionHandler(rules) {
return function(event) {
const { clientId, type, data } = event;
// Aplica regras específicas do cliente
if (rules.filter && !rules.filter(data)) return null;
// Transformação baseada nas regras
const transformed = rules.transform(data);
return {
clientId,
projectionType: rules.projectionName,
data: transformed,
version: rules.version
};
};
}
// Uso para cliente específico
const clientARules = {
filter: (data) => data.total >= 1000,
transform: (data) => ({ amount: data.total, commission: data.total * 0.05 }),
projectionName: 'high_value_orders',
version: 1
};
const handlerForClientA = createProjectionHandler(clientARules);
4.3. Cache e atualização incremental: evitando reprocessamento total
// Atualização incremental com checkpoint
function processIncremental(clientId, projectionName, lastProcessedEventId) {
const events = getEventsSince(clientId, lastProcessedEventId);
const rules = getProjectionRules(clientId, projectionName);
for (const event of events) {
const projection = createProjectionHandler(rules)(event);
if (projection) {
upsertProjection(projection);
}
}
updateCheckpoint(clientId, projectionName, events[events.length - 1].id);
}
5. Armazenamento e consulta das projeções
5.1. Modelos de banco: tabelas separadas por cliente vs coluna discriminadora
Opção 1 — Tabelas separadas por cliente:
CREATE TABLE projections_client_a (
id UUID PRIMARY KEY,
order_id VARCHAR(50),
amount DECIMAL(10,2),
commission_amount DECIMAL(10,2),
processed_at TIMESTAMP
);
CREATE TABLE projections_client_b (
id UUID PRIMARY KEY,
order_id VARCHAR(50),
customer_name VARCHAR(100),
total_items INTEGER,
delivery_estimate DATE
);
Opção 2 — Coluna discriminadora:
CREATE TABLE projections (
id UUID PRIMARY KEY,
client_id VARCHAR(20),
projection_type VARCHAR(50),
data JSONB,
created_at TIMESTAMP
);
CREATE INDEX idx_client_projection ON projections (client_id, projection_type);
5.2. Estratégias de indexação para consultas frequentes e específicas
- Índices compostos por
(client_id, projection_type, created_at) - Índices parciais para projeções mais consultadas
- Índices funcionais para consultas em campos JSONB (quando aplicável)
5.3. Exemplo de consulta de projeção customizada
-- Consulta para obter pedidos de alto valor do Cliente A
SELECT * FROM projections_client_a
WHERE amount > 5000
ORDER BY processed_at DESC
LIMIT 50;
-- Consulta genérica usando coluna discriminadora
SELECT data->>'customer_name' AS name,
data->>'total_items' AS items
FROM projections
WHERE client_id = 'B'
AND projection_type = 'order_summary'
AND created_at >= '2024-01-01';
6. Versionamento e evolução de projeções por cliente
6.1. Como lidar com mudanças de requisitos sem quebrar projeções existentes
- Manter versões anteriores da projeção ativas até que todos os consumidores migrem
- Usar versionamento semântico nos contratos de projeção (ex:
v1,v2) - Registrar metadados de versão em cada registro de projeção
6.2. Estratégias de migração: reconstrução total vs migração por snapshot
Reconstrução total: reprocessa todos os eventos desde o início — garante consistência, mas é lenta.
Migração por snapshot: salva periodicamente o estado da projeção e aplica eventos incrementais a partir do snapshot mais recente.
6.3. Controle de compatibilidade entre versões de projeção e eventos
// Verificação de compatibilidade antes de aplicar evento
function canApplyEvent(eventVersion, projectionVersion) {
const compatibilityMatrix = {
'v1': { minEventVersion: '1.0', maxEventVersion: '2.0' },
'v2': { minEventVersion: '1.5', maxEventVersion: '3.0' }
};
const rules = compatibilityMatrix[projectionVersion];
return eventVersion >= rules.minEventVersion &&
eventVersion <= rules.maxEventVersion;
}
7. Monitoramento e governança de projeções customizadas
7.1. Métricas de latência, consistência e custo de armazenamento por cliente
- Latência: tempo entre evento ocorrer e projeção estar disponível
- Consistência: número de eventos não processados por projeção
- Custo: tamanho do armazenamento e custo computacional por cliente
7.2. Ferramentas para rastrear quais projeções estão em uso
- Logs de acesso às projeções com metadados de cliente e consulta
- Dashboard com contagem de consultas por projeção
- Sistema de heartbeat que registra última consulta de cada projeção
7.3. Políticas de lifecycle: expurgo, descontinuação e auditoria de projeções
// Política de lifecycle para projeções customizadas
const lifecyclePolicy = {
'active': { maxAge: 'unlimited', notifyBeforeExpiry: false },
'deprecated': { maxAge: '90 days', notifyBeforeExpiry: 30 },
'expired': { maxAge: '30 days', action: 'delete' }
};
function evaluateProjectionLifecycle(clientId, projectionName) {
const status = getProjectionStatus(clientId, projectionName);
const lastAccess = getLastAccessDate(clientId, projectionName);
const daysSinceLastAccess = daysBetween(new Date(), lastAccess);
if (daysSinceLastAccess > 180 && status === 'active') {
setProjectionStatus(clientId, projectionName, 'deprecated');
notifyClient(clientId, projectionName, 'deprecation_warning');
}
if (daysSinceLastAccess > 270 && status === 'deprecated') {
archiveProjection(clientId, projectionName);
}
}
Referências
- Event Sourcing Pattern - Microsoft Docs — Documentação oficial sobre event sourcing e sua relação com read models.
- CQRS Pattern - Martin Fowler — Artigo fundamental sobre Command Query Responsibility Segregation.
- Materialized View Pattern - AWS Documentation — Guia prático sobre materialized views em arquiteturas orientadas a eventos.
- Event-Driven Architecture - Confluent Blog — Artigo detalhado sobre arquitetura orientada a eventos e projeções customizadas.
- Multi-Tenant Data Patterns - Microsoft Learn — Estratégias de armazenamento multi-tenant para projeções customizadas.
- Projections in Event Sourcing - Greg Young — Palestra técnica sobre implementação de projeções em sistemas event sourced.
- Read Model Optimization - Redis Documentation — Como usar Redis como armazenamento para read models de alta performance.