Skip to main content

Pesquisa — Tradeoff: Coluna array de sizes em CartRecommendedItems vs JOIN em ProductStockStores

Contexto: Task #193232 — Retorno de RecommendedItemsInfoDTO com campo Sizes (lista de tamanho + estoque na loja)
Data: 2026-05-14


Problema

O RecommendedItemsInfoDTO precisa retornar, para cada item recomendado, a lista de tamanhos disponíveis com estoque na loja (Sizes: [{ size, quantity }]). Há duas abordagens estruturais:

  • Alternativa A: Persistir os tamanhos/estoques como coluna serializada (array/JSON) diretamente na tabela CartRecommendedItems, snapshotando no momento da criação.
  • Alternativa B: Buscar os tamanhos e estoques em tempo real via JOIN em ProductStockStores no momento do GET.

Análise das alternativas

Alternativa A — Coluna serializada (snapshot)

Como funciona

No momento em que o vendedor cria a recomendação (zzapp), o sistema lê ProductStockStores para StoreId + Sku e persiste o resultado como coluna jsonb ou text serializado em CartRecommendedItems.

-- Exemplo de estrutura
CartRecommendedItems (
Id, CartId, ProductId, StoreId, Price, FullPrice,
Discount, DiscountValue,
SizesSnapshot jsonb -- [{"size":"P","quantity":3}, {"size":"M","quantity":1}]
)

Vantagens

  • GET sem JOIN extra: o handler lê apenas CartRecommendedItems + Products; ProductStockStores não é tocada no momento do GET.
  • Consistência pontual garantida: o cliente vê exatamente o estoque que o vendedor enxergava ao recomendar. Comportamento determinístico.
  • Resiliente a alterações de estoque durante a janela: se outro cliente comprar o mesmo tamanho nos 60 minutos seguintes, o zzlink ainda exibe o tamanho — mas a validação de estoque real acontece no POST de adicionar item.

Desvantagens

  • Staleness real: o snapshot fica desatualizado conforme vendas ocorrem. O cliente pode ver tamanho "M" disponível, clicar em adicionar, e receber erro de sem estoque no POST.
  • Custo de escrita aumentado: a criação da recomendação passa a exigir uma query extra em ProductStockStores + serialização.
  • Coluna de tipo complexo: jsonb ou text serializado foge do padrão das entidades do projeto (todas colunas são tipos primitivos no ORM mapeado). Introduz deserialização manual no Dapper.
  • Inconsistência estrutural: se ProductStockStores mudar de schema, o snapshot histórico fica incompatível silenciosamente.

Alternativa B — JOIN em ProductStockStores no GET (tempo real)

Como funciona

O GetRecommendedItemsAsync faz JOIN em ProductStockStores filtrando por StoreId (disponível via CartRecommendedItems.StoreId) e Sku, retornando os tamanhos com Quantity > 0.

SELECT
CRI."Id", CRI."ProductId", CRI."Price", CRI."FullPrice",
CRI."Discount", CRI."DiscountValue",
P."Sku", P."Name", P."Description",
PP."Url" as ImageUrl, PP."UrlThumbnail",
PSS."Size", PSS."Quantity"
FROM {schema}."CartRecommendedItems" CRI
INNER JOIN {schema}."Products" P ON P."Id" = CRI."ProductId"
LEFT JOIN {schema}."ProductPhotos" PP ON PP."ProductId" = P."Id"
INNER JOIN {schema}."ProductStockStores" PSS
ON PSS."Sku" = P."Sku"
AND PSS."StoreId" = CRI."StoreId"
AND PSS."Quantity" > 0
WHERE CRI."CartId" = @cartId;

Vantagens

  • Estoque sempre atual: o cliente vê exatamente o que está disponível no momento do acesso ao zzlink. Reduz UX de "adicionar e receber erro".
  • Sem dado duplicado: ProductStockStores é a fonte única de verdade para estoque. Não há risco de divergência entre snapshot e realidade.
  • Schema simples: CartRecommendedItems fica com colunas primitivas apenas — consistente com o padrão do ORM do projeto.
  • Padrão já existente: JOINs em ProductStockStores já ocorrem em outros fluxos do codebase (validações de estoque na API de cart).

Desvantagens

  • Risco de divergência na janela de 1 hora: um tamanho exibido no GET pode sumir antes do cliente clicar em adicionar. A UX deve tratar o erro 400 do POST com mensagem clara ("produto sem estoque na loja").
  • Query ligeiramente mais complexa: multi-join com Products, ProductPhotos e ProductStockStores. Ainda flat (sem multi-map), mas com mais colunas.
  • Sem histórico do estado no momento da recomendação: o vendedor recomendou com base em um estoque que pode não existir mais. Não há rastro do que ele via.

Risco crítico: Divergência de estoque entre recomendação e loja

Este risco existe nas duas alternativas, mas de formas diferentes:

CenárioAlt A (snapshot)Alt B (tempo real)
Cliente vê tamanho disponívelSim (pode ser stale)Sim (real no momento do GET)
Tamanho some antes do cliente adicionarExibido, erro no POSTDepende do timing: pode não aparecer no GET seguinte
Tamanho aparece no GET mas some antes do POSTPossível (snapshot antigo)Possível (race condition < 1h)
Mitigação necessáriaPOST valida estoque real semprePOST valida estoque real sempre

Conclusão do risco: em ambas as alternativas, o POST de adicionar item deve sempre validar estoque em tempo real na API de cart. O campo Sizes do GET é apenas informativo/UX — nunca é a fonte de verdade para a transação.


Comparativo resumido

CritérioAlt A (snapshot)Alt B (JOIN tempo real)
Precisão do estoque exibidoBaixa (stale)Alta (atual)
Complexidade da tabelaAlta (jsonb)Baixa (primitivos)
Complexidade da query GETBaixaMédia
Complexidade da escrita (POST recomendação)AltaBaixa
Consistência com padrão ORM do projetoNãoSim
UX: divergência visível ao clienteAlta probabilidadeBaixa probabilidade
Rastreabilidade do estoque no momento da recomendaçãoSimNão

Recomendação

Alternativa B — JOIN em ProductStockStores no GET.

Os principais motivos:

  1. O estoque exibido ao cliente deve ser o mais atual possível — reduz fricção no fluxo de upsell.
  2. jsonb em CartRecommendedItems quebra o padrão do ORM e introduz deserialização manual sem precedente no projeto.
  3. Em ambas as alternativas, o POST precisa validar estoque real de qualquer forma — o snapshot não elimina a validação.
  4. A rastreabilidade do estado no momento da recomendação (única vantagem real do snapshot) não foi identificada como requisito de negócio.

ADR correspondente: ADR-003-sizes-join-vs-snapshot.md