Padrões de dados em arquiteturas orientadas a eventos

1. Fundamentos da modelagem de eventos

Em arquiteturas orientadas a eventos, o evento é a unidade atômica de dados. Cada evento representa uma mudança de estado significativa no sistema e deve conter três camadas essenciais: o envelope (metadados de transporte), os metadados (identificadores, timestamps, versão de schema) e o payload (dados do domínio).

{
  "envelope": {
    "eventId": "evt-20250320-a1b2c3",
    "eventType": "OrderCreated",
    "timestamp": "2025-03-20T14:30:00Z",
    "source": "order-service",
    "schemaVersion": 2
  },
  "data": {
    "orderId": "ORD-98765",
    "customerId": "CUST-1234",
    "items": [
      {"productId": "PROD-A1", "quantity": 2, "price": 49.90}
    ],
    "totalAmount": 99.80
  }
}

A granularidade dos eventos deve ser cuidadosamente definida. Eventos de domínio representam fatos de negócio (ex: OrderShipped), eventos de integração carregam dados para sistemas externos, e eventos de estado transportam snapshots completos de entidades. O versionamento de esquemas deve seguir regras de compatibilidade: forward (leitores antigos processam novos eventos) e backward (novos leitores processam eventos antigos).

2. Padrões de roteamento e entrega de eventos

O modelo publish/subscribe com tópicos e partições é o padrão mais difundido. A escolha da chave de partição determina a ordenação e o paralelismo:

Tópico: orders
Partição 0: chave "customer-123" → eventos do cliente 123 em ordem
Partição 1: chave "customer-456" → eventos do cliente 456 em ordem
Partição 2: chave "customer-789" → eventos do cliente 789 em ordem

O content-based routing permite que consumidores recebam apenas eventos relevantes. Em sistemas como Apache Kafka, isso é implementado via filtros no consumidor ou usando topologias de stream processing. As garantias de entrega variam: "pelo menos uma vez" é o padrão mais comum, mas "exatamente uma vez" requer idempotência no produtor e transações no consumidor.

// Exemplo de filtragem baseada em conteúdo
consumer.subscribe("orders")
consumer.filter(event -> event.type == "OrderShipped" && event.amount > 1000)

3. Estratégias de serialização e schema registry

A serialização eficiente é crítica para performance. Avro oferece schemas compactos com evolução controlada, Protobuf é ideal para alta performance e tipagem forte, e JSON Schema prioriza legibilidade e interoperabilidade.

// Schema Avro para OrderCreated
{
  "type": "record",
  "name": "OrderCreated",
  "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "customerId", "type": "string"},
    {"name": "items", "type": {"type": "array", "items": "Item"}},
    {"name": "totalAmount", "type": "double"}
  ]
}

O Schema Registry centralizado (como Confluent Schema Registry) gerencia a evolução dos schemas. As regras de compatibilidade definem como mudanças são permitidas:

  • BACKWARD: novos schemas podem ler dados escritos com schemas antigos
  • FORWARD: schemas antigos podem ler dados escritos com novos schemas
  • FULL: ambos os sentidos são suportados
  • NONE: sem restrições de compatibilidade

4. Padrões de armazenamento e replay de eventos

Event Sourcing armazena o estado como uma sequência imutável de eventos. Cada evento é append-only e nunca é alterado ou deletado. O estado atual é reconstruído aplicando todos os eventos em ordem.

// Log de eventos para uma conta bancária
Event[1]: AccountCreated(accountId: "ACC-001", owner: "João")
Event[2]: DepositMade(accountId: "ACC-001", amount: 1000)
Event[3]: WithdrawalMade(accountId: "ACC-001", amount: 200)
// Estado atual: saldo = 800

O log compactado mantém apenas o último evento para cada chave, permitindo reconstruir o estado atual sem reprocessar todo o histórico. O replay de eventos é usado para reconstruir projeções, auditar mudanças ou popular novos sistemas.

5. Padrões de transformação e enriquecimento de dados

Stream processing permite transformar eventos em tempo real. Janelas de tempo (tumbling, sliding, session) agrupam eventos para agregações:

// Agregação em janela de 5 minutos
stream.window(TumblingWindow.of(Duration.ofMinutes(5)))
      .groupBy(event -> event.productId)
      .aggregate(() -> 0, (key, event, count) -> count + event.quantity)

O enriquecimento de eventos combina dados de múltiplas fontes. Um evento de pedido pode ser enriquecido com dados do cliente de uma tabela de referência ou com histórico de compras via join com um tópico de eventos passados.

// Enriquecimento com lookup em tabela de referência
KStream<String, Order> orders = builder.stream("orders");
KTable<String, Customer> customers = builder.table("customers");

KStream<String, EnrichedOrder> enriched = orders.join(
    customers,
    (order, customer) -> new EnrichedOrder(order, customer)
);

6. Padrões de consistência e transações em eventos

O Saga Pattern gerencia transações distribuídas sem locking global. Cada etapa publica um evento; se uma falha ocorre, eventos de compensação desfazem as etapas anteriores:

// Saga de pedido com compensação
1. Order Service: OrderCreated → evento publicado
2. Payment Service: PaymentProcessed → sucesso
3. Inventory Service: InventoryReserved → falha!
4. Payment Service: PaymentRefunded → evento de compensação
5. Order Service: OrderCancelled → finalização

Consistência eventual é o modelo padrão em sistemas orientados a eventos. Atualizações propagam-se assincronamente, e o sistema eventualmente converge. Dead Letter Queues (DLQs) capturam eventos malformados ou que falharam repetidamente, permitindo análise posterior e reprocessamento manual.

// Tratamento de eventos malformados
try {
    processEvent(event);
} catch (DeserializationException e) {
    deadLetterQueue.send(event, "Falha na desserialização: " + e.getMessage());
}

7. Padrões de observabilidade e governança de dados

O rastreamento de eventos usa IDs de correlação globais e tracing distribuído (como OpenTelemetry) para seguir um fluxo completo através de múltiplos serviços:

// Headers de tracing em eventos
{
  "traceId": "abc123def456",
  "spanId": "span-order-001",
  "correlationId": "corr-user-session-789"
}

Métricas essenciais incluem latência (tempo entre publicação e consumo), throughput (eventos por segundo) e taxas de erro (eventos rejeitados ou com falha). Um catálogo de eventos documenta todos os tipos de eventos, seus schemas, produtores e consumidores, facilitando a descoberta e governança.

// Métricas de pipeline de eventos
{
  "topic": "orders",
  "producerLatency": 2.3ms,
  "consumerLag": 150ms,
  "throughput": 4500 events/s,
  "errorRate": 0.02%
}

Conclusão

Padrões de dados em arquiteturas orientadas a eventos exigem decisões cuidadosas sobre modelagem, serialização, armazenamento e transformação. A escolha correta entre consistência eventual e forte, o uso de Schema Registry para evolução controlada, e a implementação de observabilidade robusta são fundamentais para sistemas escaláveis e resilientes. O catálogo de eventos e a linhagem de dados garantem que a governança acompanhe a complexidade do ecossistema orientado a eventos.

Referências