Event versioning: evoluindo schemas sem quebrar consumidores

1. Fundamentos da Evolução de Schemas

1.1. O dilema da mudança: acoplamento temporal entre produtores e consumidores

Em sistemas orientados a eventos, produtores e consumidores evoluem em ritmos diferentes. Um produtor pode precisar adicionar campos para atender novos requisitos de negócio, enquanto consumidores podem levar semanas ou meses para serem atualizados. Esse desalinhamento temporal cria um acoplamento frágil: qualquer mudança no schema do evento pode quebrar consumidores que ainda esperam a estrutura antiga.

1.2. Compatibilidade direta (forward) vs. compatibilidade reversa (backward)

  • Compatibilidade backward: consumidores antigos conseguem processar eventos novos. Exige que campos adicionados sejam opcionais ou tenham valores default.
  • Compatibilidade forward: consumidores novos conseguem processar eventos antigos. Exige que campos desconhecidos sejam ignorados.

1.3. Implicações da falta de versionamento

Sem versionamento adequado, os sintomas são clássicos:

  • Filas mortas acumulando eventos não processáveis
  • Falhas silenciosas onde consumidores ignoram campos críticos
  • Retrabalho manual para reprocessar eventos corrompidos
  • Rollbacks emergenciais que afetam todo o pipeline

2. Estratégias de Versionamento de Eventos

2.1. Versionamento no schema

A abordagem mais explícita: criar tipos separados para cada versão.

event OrderPlacedV1 {
  orderId: string
  customerId: string
  totalAmount: decimal
}

event OrderPlacedV2 {
  orderId: string
  customerId: string
  totalAmount: decimal
  discountCode: string?
}

Prós: clareza total sobre qual versão está sendo usada. Contras: duplicação de código e proliferação de tipos.

2.2. Versionamento no campo de metadados

Usar um campo eventVersion dentro de um envelope padronizado.

{
  "eventType": "OrderPlaced",
  "eventVersion": 2,
  "timestamp": "2024-01-15T10:30:00Z",
  "payload": {
    "orderId": "ORD-123",
    "customerId": "CUST-456",
    "totalAmount": 299.90,
    "discountCode": "PROMO10"
  }
}

Prós: flexibilidade para evoluir sem criar novos tipos. Contras: consumidores precisam interpretar a versão e aplicar lógica condicional.

2.3. Versionamento semântico de eventos

Aplicar regras de major/minor/patch:

  • Major (v2.0.0): mudança incompatível (remoção de campo obrigatório)
  • Minor (v1.1.0): adição backward-compatível (campo opcional)
  • Patch (v1.0.1): correção sem alteração de schema

3. Compatibilidade Backward (Consumidores Antigos com Eventos Novos)

3.1. Adição segura de campos opcionais

// Schema V1 (original)
event OrderPlaced {
  orderId: string
  customerId: string
  totalAmount: decimal
}

// Schema V2 (backward compatível)
event OrderPlaced {
  orderId: string
  customerId: string
  totalAmount: decimal
  discountCode: string?  // novo campo opcional
}

Consumidores V1 simplesmente ignoram discountCode.

3.2. Remoção gradual de campos

// Schema V2 (depreciação)
event OrderPlaced {
  orderId: string
  customerId: string
  totalAmount: decimal
  discountCode: string?
  shippingAddress: string?  // deprecated, manter por 60 dias
}

// Schema V3 (remoção completa)
event OrderPlaced {
  orderId: string
  customerId: string
  totalAmount: decimal
  discountCode: string?
}

3.3. Renomeação e mudança de tipo

Usar aliasing para renomear campos sem quebrar consumidores:

// Schema V1 (original)
event UserCreated {
  userName: string
}

// Schema V2 (aliasing)
event UserCreated {
  userName: string   // manter por compatibilidade
  displayName: string  // novo nome
}

4. Compatibilidade Forward (Consumidores Novos com Eventos Antigos)

4.1. Uso de schemas abertos

Schemas que permitem campos adicionais não declarados:

event FlexibleEvent {
  eventType: string
  eventVersion: int
  // open content: campos adicionais são permitidos
  additionalProperties: true
}

4.2. Versionamento de envelope

Estrutura que separa metadados do payload:

{
  "envelope": {
    "eventType": "OrderPlaced",
    "eventVersion": 1,
    "schemaId": "order-placed-v1"
  },
  "data": {
    "orderId": "ORD-123",
    "customerId": "CUST-456"
  }
}

Consumidores novos usam schemaId para buscar o schema correto.

4.3. Estratégias de upgrade

  • Blue/green deployment: manter consumidores antigos rodando até que todos migrem
  • Canary release: direcionar 10% do tráfego para consumidores novos, monitorar erros

5. Ferramentas e Governança com Schema Registry

5.1. Papel do schema registry

O schema registry atua como autoridade central que valida automaticamente a compatibilidade entre versões:

# Registro do schema V1
schema-registry register --subject OrderPlaced-value --schema order-placed-v1.avsc

# Tentativa de registro do schema V2
schema-registry register --subject OrderPlaced-value --schema order-placed-v2.avsc
# Validação automática: backward compatível? Sim -> Registro aprovado

5.2. Configuração de regras de evolução

  • BACKWARD: consumidores antigos processam eventos novos (default)
  • FORWARD: consumidores novos processam eventos antigos
  • FULL: ambas as direções
  • NONE: sem validação (uso interno, temporário)

5.3. Ciclo de vida de schemas

1. Registro: schema V1 ativo
2. Evolução: schema V2 registrado com regra BACKWARD
3. Depreciação: schema V1 marcado como deprecated (data de expiração)
4. Remoção: schema V1 removido após janela de transição (ex.: 90 dias)

6. Casos Complexos e Anti-Padrões

6.1. Split de evento

Quando um evento precisa ser dividido em múltiplos eventos:

// Evento original
event OrderPlaced {
  orderId: string
  customerId: string
  paymentInfo: PaymentDetails
  shippingInfo: ShippingDetails
}

// Eventos resultantes
event OrderPlaced {
  orderId: string
  customerId: string
}

event PaymentProcessed {
  orderId: string
  paymentInfo: PaymentDetails
}

event OrderShipped {
  orderId: string
  shippingInfo: ShippingDetails
}

6.2. Fusão de eventos

Correlacionar eventos usando correlationId e janelas temporais:

// Eventos separados
event PaymentReceived {
  correlationId: "CORR-001"
  amount: 299.90
}

event OrderConfirmed {
  correlationId: "CORR-001"
  orderId: "ORD-123"
}

// Fusão via stream processing (janela de 5 minutos)
SELECT correlationId, amount, orderId
FROM PaymentReceived.window(5 minutes) AS p
JOIN OrderConfirmed.window(5 minutes) AS o
ON p.correlationId = o.correlationId

6.3. Anti-padrões

  • Versionamento no nome do tópico: orders-v1, orders-v2 — duplica infraestrutura
  • Ausência de metadados: sem eventVersion ou schemaId, impossível rastrear versão
  • Quebra silenciosa: remover campos sem depreciação prévia

7. Exemplo Prático: Evolução de um Evento de Pedido

7.1. Cenário inicial

// Schema V1 - OrderPlaced
event OrderPlaced {
  orderId: string (obrigatório)
  customerId: string (obrigatório)
  totalAmount: decimal (obrigatório)
  shippingAddress: string (obrigatório)
}

7.2. Evolução 1: adição de discountCode

// Schema V2 - OrderPlaced (backward compatível)
event OrderPlaced {
  orderId: string (obrigatório)
  customerId: string (obrigatório)
  totalAmount: decimal (obrigatório)
  shippingAddress: string (obrigatório)
  discountCode: string? (opcional, novo campo)
}

Consumidores V1 continuam funcionando — ignoram discountCode.

7.3. Evolução 2: remoção gradual de shippingAddress

// Schema V3 - OrderPlaced (depreciação de shippingAddress)
event OrderPlaced {
  orderId: string (obrigatório)
  customerId: string (obrigatório)
  totalAmount: decimal (obrigatório)
  shippingAddress: string? (deprecated - manter por 60 dias)
  discountCode: string? (opcional)
}

// Schema V4 - OrderPlaced (remoção completa)
event OrderPlaced {
  orderId: string (obrigatório)
  customerId: string (obrigatório)
  totalAmount: decimal (obrigatório)
  discountCode: string? (opcional)
}

Cronograma de migração:
- Dia 0: registrar schema V3 com shippingAddress deprecated
- Dia 30: notificar consumidores sobre a remoção iminente
- Dia 60: registrar schema V4 sem shippingAddress
- Dia 90: remover schema V3 do registry

8. Considerações Finais e Boas Práticas

8.1. Prefira adições a remoções

Cada remoção de campo é uma potencial quebra. Sempre que possível, adicione campos opcionais em vez de modificar ou remover existentes.

8.2. Documente mudanças em changelogs

## Changelog de Schemas
### 2024-01-15 - OrderPlaced V2
- Adicionado campo opcional: discountCode (string?)
- Compatibilidade: backward

### 2024-03-01 - OrderPlaced V3
- Marcado como deprecated: shippingAddress
- Data de remoção prevista: 2024-05-01

8.3. Automatize validação no CI/CD

# pipeline.yml
steps:
  - name: Validate Schema Compatibility
    run: |
      schema-registry compatibility-check \
        --subject OrderPlaced-value \
        --schema order-placed-v3.avsc \
        --type BACKWARD

A automação evita que mudanças incompatíveis cheguem à produção.

Referências