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
ProductStockStoresno 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;ProductStockStoresnã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:
jsonboutextserializado 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
ProductStockStoresmudar 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:
CartRecommendedItemsfica com colunas primitivas apenas — consistente com o padrão do ORM do projeto. - Padrão já existente: JOINs em
ProductStockStoresjá 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,ProductPhotoseProductStockStores. 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ário | Alt A (snapshot) | Alt B (tempo real) |
|---|---|---|
| Cliente vê tamanho disponível | Sim (pode ser stale) | Sim (real no momento do GET) |
| Tamanho some antes do cliente adicionar | Exibido, erro no POST | Depende do timing: pode não aparecer no GET seguinte |
| Tamanho aparece no GET mas some antes do POST | Possível (snapshot antigo) | Possível (race condition < 1h) |
| Mitigação necessária | POST valida estoque real sempre | POST 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ério | Alt A (snapshot) | Alt B (JOIN tempo real) |
|---|---|---|
| Precisão do estoque exibido | Baixa (stale) | Alta (atual) |
| Complexidade da tabela | Alta (jsonb) | Baixa (primitivos) |
| Complexidade da query GET | Baixa | Média |
| Complexidade da escrita (POST recomendação) | Alta | Baixa |
| Consistência com padrão ORM do projeto | Não | Sim |
| UX: divergência visível ao cliente | Alta probabilidade | Baixa probabilidade |
| Rastreabilidade do estoque no momento da recomendação | Sim | Não |
Recomendação
Alternativa B — JOIN em ProductStockStores no GET.
Os principais motivos:
- O estoque exibido ao cliente deve ser o mais atual possível — reduz fricção no fluxo de upsell.
jsonbemCartRecommendedItemsquebra o padrão do ORM e introduz deserialização manual sem precedente no projeto.- Em ambas as alternativas, o POST precisa validar estoque real de qualquer forma — o snapshot não elimina a validação.
- 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