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
GETcom 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:
- O BFF resolve
sku → skuId(cache 5min) e chamapriceservice. priceservicevalida (ex.: período de promoção não sobrepõe outro), grava e publicaPriceUpdatedna plataforma de streaming.- O BFF recebe
202 Accepteddo downstream e repassa ao cliente. pricequeryserviceconsome 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
productservicee oadservice(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:
- Verifique o
202: se o write retornou202, o evento foi aceito pelo serviço de escrita. - Aguarde 5 segundos e faça um novo
GET. Se ainda não refletiu, há problema no consumer. - Consulte o Grafana do queryservice (através do SRE da Gubee) para ver o lag do consumer de eventos.
- 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.