Estratégias de versionamento de eventos em event sourcing

1. Fundamentos do versionamento de eventos em event sourcing

1.1. O problema da evolução de esquemas em streams de eventos imutáveis

Event sourcing impõe um desafio fundamental: eventos são registros imutáveis e históricos. Quando a lógica de negócio evolui, o schema dos eventos precisa mudar, mas os eventos antigos não podem ser alterados. Sem uma estratégia de versionamento, sistemas quebram ao tentar desserializar eventos antigos com schemas novos.

Considere um evento OrderPlaced que inicialmente continha apenas orderId e customerId:

Evento v1:
{
  "eventType": "OrderPlaced",
  "version": 1,
  "data": {
    "orderId": "ORD-001",
    "customerId": "CUST-123"
  }
}

Após uma mudança de requisito, precisamos adicionar totalAmount. Um novo consumidor tentando ler o evento v1 falharia sem versionamento.

1.2. Compatibilidade direta vs. reversa: impacto na leitura de eventos históricos

  • Compatibilidade direta (forward compatibility): Consumidores escritos para uma versão antiga conseguem ler eventos novos. Útil quando consumidores são atualizados antes dos produtores.
  • Compatibilidade reversa (backward compatibility): Consumidores novos conseguem ler eventos antigos. Essencial para event sourcing, pois novos serviços precisam processar todo o histórico.

A escolha impacta diretamente a estratégia de versionamento. Em sistemas de event sourcing, a compatibilidade reversa é quase sempre obrigatória.

1.3. Versionamento semântico de eventos (major/minor/patch) e quebra de contrato

Adote versionamento semântico para eventos:

  • Major: Mudanças que quebram compatibilidade reversa (remoção de campos, renomeação)
  • Minor: Adições de campos opcionais que não quebram consumidores existentes
  • Patch: Correções em metadados ou documentação sem alteração de schema
Evento v2.0.0 (major - quebra contrato):
{
  "eventType": "OrderPlaced",
  "version": "2.0.0",
  "data": {
    "orderId": "ORD-001",
    "customerId": "CUST-123",
    "totalAmount": 250.00,
    "currency": "BRL"
  }
}

2. Estratégias de evolução de eventos: upgrade e migração

2.1. Upcasting (transformação sob demanda)

Upcasting aplica funções de transformação no momento da leitura, sem alterar eventos no armazenamento. É a estratégia mais comum em event sourcing.

// Upcaster para evento OrderPlaced v1 -> v2
function upcastOrderPlacedV1ToV2(event):
  if event.version == 1:
    return {
      ...event,
      version: "2.0.0",
      data: {
        ...event.data,
        totalAmount: 0.00,      // valor padrão para eventos antigos
        currency: "USD"          // valor padrão
      }
    }
  return event  // já na versão atual

Vantagens: Não modifica o armazenamento, fácil de implementar, baixo custo operacional.
Desvantagens: Overhead de processamento a cada leitura, acúmulo de funções de upcast ao longo do tempo.

2.2. Migração de eventos (event migration)

Reescrita física de eventos antigos para o schema atual, geralmente executada em lote durante janelas de manutenção.

// Script de migração em lote
1. Selecionar todos eventos OrderPlaced com version < 2.0.0
2. Para cada evento, aplicar transformação:
   - Adicionar campo totalAmount com valor 0.00
   - Adicionar campo currency com valor "USD"
3. Substituir evento antigo pelo novo no event store
4. Atualizar índices e snapshots

Vantagens: Elimina a necessidade de upcasters antigos, melhora performance de leitura.
Desvantagens: Operação complexa e arriscada, requer downtime ou estratégia de blue-green deployment.

2.3. Dual-write com novas versões

Escrever simultaneamente eventos nas versões antiga e nova durante um período de transição.

// Producer escreve ambos os formatos
async function publishOrderPlaced(order):
  eventV1 = createEventV1(order)
  eventV2 = createEventV2(order)

  await eventStore.append("OrderPlaced", eventV1)
  await eventStore.append("OrderPlaced_v2", eventV2)

Vantagens: Permite migração gradual sem downtime, consumidores antigos continuam funcionando.
Desvantagens: Duplica armazenamento, complexidade de coordenação entre versões.

3. Versionamento por schema registry e serialização

3.1. Uso de Apache Avro, Protobuf ou JSON Schema com schema registry

Schema registries (como Confluent Schema Registry ou Apicurio) gerenciam versões de schemas centralizadamente.

// Schema Avro para OrderPlaced v1
{
  "type": "record",
  "name": "OrderPlaced",
  "namespace": "com.example.events",
  "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "customerId", "type": "string"}
  ]
}

// Schema Avro para OrderPlaced v2 (compatível backward)
{
  "type": "record",
  "name": "OrderPlaced",
  "namespace": "com.example.events",
  "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "customerId", "type": "string"},
    {"name": "totalAmount", "type": "double", "default": 0.0},
    {"name": "currency", "type": "string", "default": "USD"}
  ]
}

3.2. Estratégias de compatibilidade e implicações

  • BACKWARD: Consumidores novos leem eventos antigos (default no Confluent Registry). Campos novos devem ter default.
  • FORWARD: Consumidores antigos leem eventos novos. Permite remoção de campos.
  • FULL: BACKWARD + FORWARD simultaneamente. Mais restritivo.
  • NONE: Sem verificação de compatibilidade. Risco de quebra em produção.

3.3. Versionamento de schemas aninhados e eventos polimórficos

Eventos podem conter objetos aninhados ou ser polimórficos (um evento base com subtipos). O versionamento deve considerar toda a árvore de schemas.

// Evento polimórfico com discriminator
{
  "eventType": "PaymentProcessed",
  "version": "1.0.0",
  "data": {
    "paymentMethod": {
      "type": "CreditCard",
      "lastFourDigits": "1234",
      "cardBrand": "Visa"
    }
  }
}

4. Versionamento de agregados e comandos associados

4.1. Versionamento de eventos derivados de mudanças na lógica de negócio

Quando a lógica do agregado muda, os eventos gerados podem precisar de novos campos. Manter uma matriz de versões ajuda a rastrear dependências.

4.2. Estratégias para evolução de comandos

Comandos que geram eventos podem ser versionados independentemente. Um comando PlaceOrderV2 pode gerar eventos OrderPlacedV2 enquanto PlaceOrderV1 continua gerando OrderPlacedV1.

4.3. Mapeamento de versões (event version matrix)

Matriz de Versões de Eventos por Agregado:
+------------------+---------+---------+---------+
| Agregado         | v1.0.0  | v2.0.0  | v3.0.0  |
+------------------+---------+---------+---------+
| OrderPlaced      | 1.0.0   | 2.0.0   | 3.0.0   |
| OrderCancelled   | 1.0.0   | 1.0.0   | 2.0.0   |
| PaymentReceived  | -       | 1.0.0   | 2.0.0   |
+------------------+---------+---------+---------+

5. Tratamento de eventos obsoletos e deprecação

5.1. Deprecação gradual com metadados

Adicione cabeçalhos de metadados para sinalizar deprecação:

{
  "eventType": "OrderPlaced",
  "version": "2.0.0",
  "metadata": {
    "deprecated": true,
    "replacement": "OrderCreated",
    "deprecationDate": "2024-06-01",
    "removalDate": "2024-12-01"
  }
}

5.2. Snapshot + compressão

Use snapshots periódicos do estado do agregado para reduzir a necessidade de reprocessar eventos antigos. Eventos anteriores ao snapshot mais recente podem ser compactados ou arquivados.

5.3. Versionamento de projeções e read models

Projeções (read models) precisam ser versionadas junto com os eventos que as alimentam. Uma projeção OrderSummaryV2 pode consumir eventos OrderPlacedV2 enquanto OrderSummaryV1 continua consumindo OrderPlacedV1.

6. Versionamento em sistemas distribuídos e concorrência

6.1. Garantia de ordem global com versionamento temporal

Use timestamps ou números de sequência global para ordenar eventos de diferentes versões. Versionamento temporal (wall clock + logical clock) ajuda a preservar a ordem causal.

6.2. Cross-region replication

Em replicação multi-região, eventos podem chegar fora de ordem. O versionamento deve incluir metadados de região e timestamp de origem para resolução de conflitos.

6.3. Múltiplos produtores concorrentes

Quando vários serviços produzem o mesmo tipo de evento, o schema registry deve ser centralizado para evitar divergências de schema entre produtores.

7. Testes e governança de versionamento de eventos

7.1. Testes de compatibilidade automatizados

Implemente testes que verificam se eventos de todas as versões podem ser desserializados corretamente:

Teste de compatibilidade reversa:
1. Criar evento v1.0.0
2. Serializar e armazenar
3. Tentar desserializar com schema v2.0.0
4. Verificar se campos com default são preenchidos
5. Verificar se campos obrigatórios existem

7.2. Estratégias de rollback

Mantenha os schemas antigos no registry por pelo menos N versões. Para rollback, basta reverter o schema ativo e reaplicar upcasters se necessário.

7.3. Governança de mudanças

Estabeleça approval gates para mudanças de schema:

  • Patch: Aprovação automática
  • Minor: Revisão de equipe
  • Major: Aprovação de arquiteto + documentação de breaking changes + comunicação a todos os consumidores

Referências