Introdução ao padrão event-carried state transfer

1. Fundamentos do Event-Carried State Transfer (ECST)

O padrão Event-Carried State Transfer (ECST) surge como uma evolução natural das arquiteturas orientadas a eventos, resolvendo um problema crítico: como distribuir dados entre microsserviços sem criar dependências síncronas de banco de dados centralizado.

Diferentemente dos eventos de notificação tradicionais — que carregam apenas um identificador e exigem que o consumidor busque dados adicionais — o ECST embute no próprio evento o estado completo do agregado no momento da publicação. Isso elimina a necessidade de consultas HTTP ou queries em bancos remotos.

A origem do padrão está ligada à popularização de sistemas distribuídos que priorizam disponibilidade e tolerância a partições (teorema CAP). Em vez de sacrificar a consistência imediata, o ECST aceita consistência eventual em troca de resiliência e baixa latência.

2. Estrutura de um Evento com Estado Transportado

Um evento ECST típico contém:

{
  "eventId": "e7b8c9d0-1234-5678-abcd-ef0123456789",
  "eventType": "OrderPlaced",
  "timestamp": "2025-04-01T14:30:00Z",
  "version": 2,
  "payload": {
    "orderId": "ORD-98765",
    "customer": {
      "id": "CUST-123",
      "name": "Maria Silva",
      "email": "maria@exemplo.com",
      "shippingAddress": "Rua A, 100"
    },
    "items": [
      {
        "productId": "PROD-456",
        "name": "Teclado Mecânico",
        "quantity": 1,
        "unitPrice": 249.90
      }
    ],
    "total": 249.90,
    "status": "confirmed"
  }
}

O versionamento do payload é essencial. Recomenda-se:

  • Adicionar campo version no envelope do evento
  • Usar Avro ou Protobuf para schemas evolutivos
  • Manter compatibilidade retroativa por pelo menos 3 versões

3. Comparação com Padrões Alternativos

Característica ECST Event Notification Event Sourcing
Dados no evento Estado completo Apenas ID Eventos de mudança
Consulta necessária Não Sim (HTTP/DB) Sim (reconstrução)
Latência de leitura Baixa Alta Alta
Complexidade Média Baixa Alta

ECST vs Event Notification: O primeiro elimina a viagem de ida-e-volta para buscar dados. No cenário de notificação, o consumidor recebe apenas "orderId": "ORD-98765" e precisa chamar um endpoint para obter os detalhes.

ECST vs Event Sourcing: No Event Sourcing puro, cada mudança é um evento atômico e o estado atual é reconstruído pelo log. No ECST, o evento já carrega o estado final, simplificando consumidores que precisam apenas de leitura.

4. Cenários de Uso Típicos

Sincronização entre microsserviços: Um serviço de catálogo publica ProductUpdated com todos os atributos. O serviço de busca e o serviço de front-end consomem o mesmo evento sem depender de um banco central.

Read models customizados: Cada cliente pode construir sua própria projeção dos dados. Um serviço de recomendações pode filtrar apenas produtos com estoque > 0 a partir dos eventos recebidos.

Cache distribuído: Dados de referência (categorias, perfis de usuário) são replicados localmente em cada serviço consumidor, eliminando chamadas de rede para leitura.

5. Desafios e Armadilhas na Implementação

Inflação do tamanho do evento: Se um agregado tem 50 campos, o evento pode chegar a 10 KB. Em alta frequência (1000 eventos/s), o throughput pode chegar a 10 MB/s, impactando rede e armazenamento.

Consistência eventual: Se o consumidor processa eventos fora de ordem, pode exibir dados desatualizados. Estratégias como timestamps monotônicos e detecção de versões antigas são necessárias.

Duplicação de dados: Cada serviço mantém sua própria cópia do estado. Isso aumenta o armazenamento total e exige coordenação para atualizações.

6. Estratégias de Versionamento e Evolução

Adição de campos opcionais: Novos campos devem ter defaults definidos no consumidor:

{
  "productId": "PROD-456",
  "name": "Teclado Mecânico",
  "price": 249.90,
  "stock": 100,
  "category": null,  // novo campo opcional
  "rating": null     // novo campo opcional
}

Schemas evolutivos com Protobuf:

syntax = "proto3";
message ProductUpdated {
  string product_id = 1;
  string name = 2;
  double price = 3;
  int32 stock = 4;
  optional string category = 5;  // campo adicionado
  optional double rating = 6;    // campo adicionado
}

Migração gradual: Publicar eventos com ambas as versões do schema por um período de transição, permitindo que consumidores antigos e novos coexistam.

7. Boas Práticas de Design e Operação

Contrato compartilhado: Manter os schemas de eventos em um repositório Git centralizado, versionado e revisado por todas as equipes.

Monitoramento: Rastrear métricas como:
- Tamanho médio do evento (alerta > 50 KB)
- Frequência de publicação por tipo
- Taxa de reprocessamento

Padrão Outbox: Garantir que o evento seja publicado atomicamente com a transação do banco de dados:

1. Iniciar transação
2. Atualizar estado no banco
3. Inserir evento na tabela outbox
4. Comitar transação
5. Publicar evento a partir da outbox

8. Exemplo Prático: Catálogo de Produtos Distribuído

Serviço Produtor (catálogo) publica:

{
  "eventId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "eventType": "ProductUpdated",
  "timestamp": "2025-04-01T15:00:00Z",
  "version": 3,
  "payload": {
    "productId": "PROD-789",
    "name": "Mouse Gamer",
    "price": 189.90,
    "stock": 50,
    "category": "Periféricos",
    "imageUrl": "https://cdn.exemplo.com/mouse.jpg",
    "description": "Mouse RGB 6 botões",
    "rating": 4.5
  }
}

Serviço Consumidor (front-end) mantém cache local:

// Exemplo de lógica do consumidor
function handleProductUpdated(event) {
    const product = event.payload;

    // Verificar versão para evitar dados antigos
    if (product.version < localCache.getVersion(product.productId)) {
        return;  // Ignorar evento desatualizado
    }

    // Atualizar cache local sem consulta ao banco
    localCache.set(product.productId, {
        name: product.name,
        price: product.price,
        stock: product.stock,
        imageUrl: product.imageUrl
    });

    // Atualizar interface do usuário
    renderProductCard(product);
}

Tratamento de falhas: O consumidor deve ser idempotente. Se o mesmo evento for processado duas vezes, o resultado deve ser o mesmo. Isso é garantido usando o productId como chave e verificando a versão.

Reprocessamento: Em caso de falha, o consumidor pode buscar eventos perdidos em um tópico de replay, desde que o produtor mantenha um log de eventos publicados.


O padrão Event-Carried State Transfer é uma ferramenta poderosa para arquiteturas de microsserviços que precisam de leituras rápidas e baixo acoplamento. Quando combinado com boas práticas de versionamento, idempotência e monitoramento, ele oferece um equilíbrio entre simplicidade e robustez.

Referências