Pular para conteúdo

CQRS e consistência eventual

Preço e estoque na Gubee seguem o padrão CQRS (Command Query Responsibility Segregation): as escritas vão para um serviço dedicado, e as leituras vêm de outro serviço alimentado via plataforma de streaming de eventos. Isso significa que após um PUT bem sucedido (202 Accepted), a leitura imediata pode não refletir o novo valor.

Por que CQRS?

A separação permite:

  • Escrita otimizada para validação e eventos: o serviço de escrita aplica regras de negócio, escreve no Mongo e publica o evento de domínio.
  • Leitura otimizada para consulta: o serviço de leitura mantém uma visão desnormalizada e indexada para responder rápido a GET com filtros.
  • Isolamento de carga: uma rajada de escritas (ex.: importação) não degrada as leituras do front web.

Os pares de serviços

Domínio Escrita (Command) Leitura (Query)
Preço priceservice (PriceApi) pricequeryservice (PriceQueryApi)
Estoque stockservice (StockApi) stockqueryservice (StockQueryApi)

Configuração em application.yml (exemplo; nomes lógicos, não expostos a parceiros):

services:
  endpoints:
    priceservice:      ${PRICESERVICE_URL}
    pricequeryservice: ${PRICEQUERYSERVICE_URL}
    stockservice:      ${STOCKSERVICE_URL}
    stockqueryservice: ${STOCKQUERYSERVICE_URL}

Cada um desses nomes lógicos aponta para um microserviço interno distinto na infraestrutura da Gubee. Como parceiro, você não precisa (e não consegue) chamar esses serviços diretamente — toda leitura e escrita passa pela API pública em /integration/prices/* e /integration/stocks/*.

Fluxo de escrita

Quando você chama PUT /integration/prices/bysku:

  1. O BFF resolve sku → skuId (cache 5min) e chama priceservice.
  2. priceservice valida (ex.: período de promoção não sobrepõe outro), grava e publica PriceUpdated na plataforma de streaming.
  3. O BFF recebe 202 Accepted do downstream e repassa ao cliente.
  4. pricequeryservice consome o evento e atualiza sua visão de leitura.

Latência típica do passo 4: 200ms a 3s, dependendo do lag do consumer.

   PUT /integration/prices/bysku (sku, price)
            |
            v
   +-----------------+         +-------------------+
   |   gubee-api     |  -----> |   priceservice    |
   |   (BFF)         |  202    |   (write)         |
   +-----------------+         +---------+---------+
                                         |
                                         v
                                +-------------------+
                                |   canal de eventos     |
                                |   price.updated   |
                                +---------+---------+
                                          |
                                          v
                                +-------------------+
                                | pricequeryservice |
                                |   (read model)    |
                                +-------------------+
                                          |
                                          v
   GET /integration/prices/bysku <------- +
                  |
                  v
   (reflete novo preço após ~1s)

Implicação prática para testes de integração

Não faça write-then-read em testes automáticos sem esperar o evento. O padrão abaixo é incorreto:

// ANTI-PATTERN: leitura imediata após escrita
val sku = "SKU-DEMO-001"
priceApi.updateBySku(sku, Price(199.90))
val price = priceApi.getBySku(sku)  // pode retornar o preço antigo!
assertThat(price.value).isEqualTo(199.90)  // FLAKY

Padrões corretos

Opção A: Webhook como fonte de verdade

Configure webhook para PriceUpdated e StockUpdated (veja a referência de webhooks). Trate o webhook como confirmação canônica de que a escrita foi refletida no read model. Só então faça assertions.

// PATTERN: esperar webhook
priceApi.updateBySku(sku, Price(199.90))
webhookAwaiter.await("PriceUpdated", sku)  // bloqueia até receber o evento
val price = priceApi.getBySku(sku)
assertThat(price.value).isEqualTo(199.90)  // estável

Opção B: Poll com backoff

Faça polling do endpoint de leitura com backoff exponencial até refletir o valor esperado ou timeout (10–30s).

await.atMost(10, SECONDS).pollInterval(500, MILLISECONDS).until {
    val price = priceApi.getBySku(sku)
    price?.value == 199.90
}

Opção C: Use o 202 como confirmação suficiente

Se o seu teste não precisa validar o read model (ex.: você quer apenas confirmar que o payload foi aceito), confie no 202:

val status = priceApi.updateBySku(sku, Price(199.90))
assertThat(status).isEqualTo(202)  // suficiente para validação de contrato

Operações afetadas

Operação Comando (write) Query (read) Latência esperada
PUT /integration/prices/bysku priceservice /integration/prices/bysku (GET) 0.2–3s
PUT /integration/prices/{productId}/{skuId} priceservice /integration/prices/{platform}/{itemId} 0.2–3s
PUT /integration/stocks/bysku stockservice /integration/stocks/bysku (GET) 0.2–3s
PUT /integration/stocks/{itemId}/{warehouseId} stockservice /integration/stocks/all/{platform}/{itemId} 0.2–3s
Criação/atualização de produto productservice /integration/products/{productId} 1–10s

Produto também é eventualmente consistente entre o productservice e o adservice (anúncios são derivados via plataforma de streaming de eventos). Por isso, após criar um produto, o anúncio pode demorar até 30s para aparecer em /integration/ads/list/byOriginSkuId.

Eventos relevantes na plataforma de streaming

Os seguintes eventos são publicados e podem ser consumidos via webhook:

  • PriceUpdated (preço default ou promoção alterado)
  • StockUpdated (quantidade em warehouse alterada)
  • ProductCreated, ProductUpdated (produto criado/atualizado)
  • AdCreated, AdUpdated (anúncio derivado de produto)
  • VideoReady, VideoDeleted (mídia processada)

A fonte canônica para configuração de webhook é a referência /content/webhooks/ deste portal.

Anti-padrões

write → read → assert sem espera

Já coberto acima. Em produção pode funcionar na maioria das vezes e quebrar aleatoriamente sob carga.

Polling agressivo após write

Fazer 100 GETs por segundo para "ver quando atualizou" é desperdício e pode ser confundido com abuso. Use webhook como gatilho; GET apenas quando realmente precisar do valor atual.

Assumir ordem entre eventos

Eventos de preço e estoque são publicados em tópicos diferentes e podem chegar fora de ordem entre si. Se você precisa consistência entre os dois domínios, coordene no nível do seu serviço (ex.: saga ou outbox).

Ler do serviço errado

Se você precisar do valor mais recente possível (ex.: para checkout), consulte o endpoint /integration/* do BFF que roteia para o queryservice correspondente. Não tente chamar o priceservice diretamente — ele não expõe endpoint de leitura.

Pressumir transacionalidade entre preço e estoque

Preço e estoque são domínios independentes com serviços independentes. Não há transação distribuída entre eles. Se você atualizar preço e estoque em sequência, o evento StockUpdated pode chegar antes do PriceUpdated (ou vice-versa).

Se a sua regra de negócio exige consistência entre os dois (ex.: "só vende se preço e estoque estiverem atualizados"), coordene no nível do seu serviço com um indicador composto (ex.: campo catalogSyncVersion no seu ERP que só avança quando ambos os webhooks chegaram).

Como observar o atraso em produção

Se você suspeitar de lag excessivo entre escrita e leitura:

  1. Verifique o 202: se o write retornou 202, o evento foi aceito pelo serviço de escrita.
  2. Aguarde 5 segundos e faça um novo GET. Se ainda não refletiu, há problema no consumer.
  3. Consulte o Grafana do queryservice (através do SRE da Gubee) para ver o lag do consumer de eventos.
  4. Não faça retry do write prematuramente — isso pode duplicar eventos de domínio (embora o downstream seja idempotente, gera ruído).

Latência esperada em regime normal:

Domínio P50 P95 P99
Preço 200ms 1s 3s
Estoque 200ms 1s 3s
Produto → Anúncio 5s 15s 30s

Valores acima de P99 por mais de 5 minutos indicam incidente — abra chamado.

Próximos passos

  • SKU vs SkuId: identificadores usados nas rotas de preço e estoque.
  • Quickstart: fluxo completo com 202.
  • Ad vs Produto: como preço/estoque de produto se replica para anúncios.