01 - E-commerce (Carrinho, Pedidos, Pagamento)
video: https://youtu.be/... no frontmatter da nota.E-commerce: Carrinho, Pedidos e Pagamento
Este caso acompanha um varejista online de médio porte, com 2 milhões de clientes cadastrados e 40 mil produtos ativos, que precisa atravessar a Black Friday sem degradar a experiência de compra. Na semana da campanha o tráfego multiplica por oito: a leitura de catálogo sobe de 1.500 para 12.000 requisições por segundo e o checkout salta de 120 para 900 confirmações por segundo na abertura da venda. Os objetivos arquiteturais são três: manter o checkout disponível e previsível sob carga extrema, impedir cobrança duplicada diante de retries e timeouts, e garantir que pedido confirmado nunca desapareça. O desenho separa leitura quente de escrita crítica, integra domínios por eventos e trata idempotência como contrato de fronteira. O resultado buscado é um pico sem incidente financeiro, com degradação controlada em vez de queda total.
Requisitos e pressupostos
Os números de referência orientam cada decisão de capacidade. A plataforma opera com 12 milhões de sessões mensais e concentra 35% das vendas do mês em 48 horas de campanha. As metas de serviço acordadas com o negócio definem o que a arquitetura precisa sustentar:
- Disponibilidade de 99,95% para checkout e pagamento durante a campanha, o que admite no máximo 21 minutos de indisponibilidade no período de 48 horas.
- Latência p99 de 800 ms no checkout e de 150 ms na página de produto, medida na borda e não no serviço isolado.
- Zero cobrança duplicada: nenhuma transação financeira pode ser processada duas vezes, mesmo com retries do cliente ou do gateway.
- Carrinho ativo não pode desaparecer, mas pode ser recriado a partir do último estado persistido em caso de falha do cache.
- Cupons e descontos aplicam de forma determinística, com registro de qual regra concedeu cada desconto, porque reclamação de cupom é o chamado mais comum após a campanha.
- Painel de funil de compra atualizado por segundo durante a campanha, com alertas por etapa do checkout e não apenas por serviço.
As metas não são desejos: cada uma virou critério de aceite nos testes de carga pré-campanha, e a indisponibilidade simulada faz parte do roteiro. O erro budget do trimestre é reservado integralmente para o período, o que significa que a plataforma entra na Black Friday sem depleção de orçamento de confiabilidade.
Os pressupostos assumidos delimitam o escopo do desenho. O estoque aceita venda eventual com reconciliação posterior, porque impedir oversell a qualquer custo exigiria lock global que derruba o throughput. O antifraude é um serviço externo com SLA próprio de 300 ms no p99, e não um componente interno sob controle do time. A adquirente responde em até 3 segundos e pode falhar por timeout em picos, o que obriga desenho de compensação explícito. Por fim, 80% do tráfego de catálogo é anônimo e altamente cacheável, o que permite tirar essa carga do núcleo transacional. A integração com o ERP financeiro roda em lote noturno, então nenhuma decisão do pico depende do sistema legado em tempo real.
A matemática de capacidade sai desses números. Com hit ratio de 90% na CDN, a origem de catálogo enfrenta 1.200 requisições por segundo no pico, e o cache Redis absorve 85% do restante, deixando 180 consultas por segundo ao banco. O checkout de 900 confirmações por segundo gera cerca de 4.500 escritas por segundo somando pedido, itens, reserva e outbox, volume que um PostgreSQL particionado com conexões em pool comporta com folga quando as transações são curtas.
Arquitetura proposta
A arquitetura divide a plataforma em quatro domínios atrás de um API Gateway: catálogo, carrinho, pedidos e pagamento. Um BFF compõe as chamadas da tela de checkout em uma única requisição do cliente, reduzindo latência percebida e acoplamento do frontend aos serviços. Cada domínio possui seu armazenamento, e a integração entre eles é feita por eventos publicados com o padrão outbox, consumidos por assinantes que atualizam estoque, expedição, marketing e analytics. A tabela resume os componentes e as escolhas que os sustentam.
| Componente | Tecnologia | Responsabilidade | Justificativa |
|---|---|---|---|
| Borda | CDN + WAF | Absorve leitura de catálogo e filtra bots | Remove a carga quente antes da origem |
| Catálogo | PostgreSQL + Redis write-through | Produto, preço e vitrine | TTL curto de 5 min limita defasagem de preço |
| Carrinho | Redis Cluster, 3 shards com réplica | Sessão de compra com TTL de 24 h | Escritas frequentes por chave, custo baixo |
| Pedidos | PostgreSQL particionado por mês | Pedido, itens e reserva de estoque | Transações ACID e histórico consultável |
| Pagamento | PostgreSQL próprio + adquirente | Antifraude, autorização e captura | Isolamento de falha e escopo PCI reduzido |
| Integração | Kafka, 3 brokers, fator de replicação 3 | Eventos de domínio entre serviços | Buffer contra picos e replay de eventos |
| Analytics | Consumidor Kafka para data lake | Relatórios e comportamento de compra | Leitura do fluxo sem tocar na origem |
O vocabulário de eventos fecha o contrato entre domínios: PedidoCriado, EstoqueReservado, PagamentoAprovado, PagamentoRecusado, PedidoEnviado e ProdutoPrecoAtualizado. Cada evento carrega apenas o que o fato determina, com metadados de correlação para tracing ponta a ponta. A publicação sai da mesma transação que grava o pedido, na tabela de outbox, e um relay envia ao Kafka com confirmação de escrita. Esse arranjo elimina o fantasma do pedido criado sem evento, que deixaria estoque e expedição cegos.
A invalidação de cache segue o mesmo caminho dos eventos. O consumidor de ProdutoPrecoAtualizado remove a chave afetada do Redis e solicita invalidação na CDN por tag de página, e a próxima leitura reconstrói o estado a partir do banco. Os consumidores são idempotentes por construção: a reentrega do mesmo evento não duplica efeito, o que autoriza política de retry agressiva sem risco de efeito colateral.
Fluxo principal: do carrinho ao pedido pago
O carrinho vive inteiro no Redis como hash por sessão, com TTL de 24 horas renovado a cada interação. Cada item é um campo do hash com sku, quantidade e preço capturado na inclusão, o que evita surpresas se o catálogo mudar durante a compra. Para usuários logados, o identificador da conta substitui a sessão anônima após o login, com merge dos dois carrinhos. Um carrinho abandonado simplesmente expira e libera memória, sem job de limpeza.
As operações do carrinho são comandos diretos ao Redis, sem passagem pelo PostgreSQL, e a sequência a seguir mostra o ciclo típico de uma sessão.
# Inclusão de item: hash por sessão com preço capturado na hora
HSET carrinho:sessao-a81f item:SKU-90342 '{"sku":"SKU-90342","qtd":2,"preco":149.90}'
EXPIRE carrinho:sessao-a81f 86400 # TTL de 24 h renovado a cada interação
# Consulta da tela de carrinho: leitura única por chave
HGETALL carrinho:sessao-a81f
# Atualização de quantidade ainda no checkout
HSET carrinho:sessao-a81f item:SKU-90342 '{"sku":"SKU-90342","qtd":1,"preco":149.90}'
# Checkout confirmado: a sessão é encerrada e a memória liberada
DEL carrinho:sessao-a81f
O checkout concentra três invariantes: pedido criado uma única vez, estoque reservado atomicamente e cobrança executada uma única vez. A chave de idempotência nasce no cliente como UUID v4 e viaja no header de todas as chamadas do fluxo. O serviço de pedidos guarda essa chave com constraint única, e qualquer reenvio devolve o pedido original em vez de criar outro. Sem essa âncora, todo timeout de rede no pico viraria pedido fantasma ou cobrança dupla.
def criar_pedido(chave_idempotencia, carrinho_id, endereco):
# 1. Idempotência primeiro: constraint única em pedidos.chave_idempotencia
pedido = repo.buscar_por_idempotencia(chave_idempotencia)
if pedido:
return pedido # retry devolve o mesmo pedido, sem nova cobrança
# 2. Transação única: pedido e reserva de estoque saem juntos ou não saem
with transacao() as t:
pedido = t.inserir_pedidos(status="AGUARDANDO_PAGAMENTO",
chave_idempotencia=chave_idempotencia)
for item in carrinho.itens():
# reserva atômica: só reserva se houver saldo disponível agora
atualizado = t.executar(
"UPDATE estoque SET reservado = reservado + %s "
"WHERE sku = %s AND disponivel >= %s",
item.quantidade, item.sku, item.quantidade)
if atualizado == 0:
t.rollback() # estoque insuficiente: falha rápida e legível
raise EstoqueInsuficiente(item.sku)
t.inserir_itens(pedido.id, carrinho.itens())
# 3. Outbox: o evento só é publicado se a transação confirmou
t.inserir_outbox(evento="PedidoCriado", payload=pedido.como_evento())
return pedido
Após o PedidoCriado, o serviço de pagamento assume o fluxo. Ele consulta o antifraude com timeout de 2 segundos e circuit breaker, envia autorização à adquirente usando a mesma chave de idempotência e grava o resultado com a resposta completa da adquirente, porque conciliação posterior precisa do código original. A captura acontece em dois passos: autorização na aprovação e captura na expedição, o que permite ajustar valor de frete sem estorno. A aprovação publica PagamentoAprovado; a recusa dispara a saga de compensação, que libera a reserva de estoque e notifica o cliente. Reservas não pagas expiram em 30 minutos por job de varredura, devolvendo o saldo ao disponível.
A compensação roda a partir do evento de recusa, nunca dentro da transação original, porque desfazer é um processo separado com o mesmo rigor de auditoria.
def liberar_reserva(evento_pagamento_recusado):
# Saga de compensação: reage a PagamentoRecusado e devolve o saldo
with transacao() as t:
for item in evento_pagamento_recusado.itens():
t.executar(
"UPDATE estoque SET reservado = reservado - %s, "
"disponivel = disponivel + %s WHERE sku = %s",
item.quantidade, item.quantidade, item.sku)
# novo fato fecha o ciclo e mantém a trilha íntegra
t.inserir_outbox(evento="EstoqueLiberado",
payload=evento_pagamento_recusado.pedido())
Na abertura da campanha, um waiting room na borda regula a entrada. Clientes excedentes recebem página de fila com posição estimada e são liberados em ritmo que mantém o checkout interno em 70% da capacidade, o que deixa folga para absorver retry em cascata sem dominó. A fila é ordenada por chegada e o tempo médio de espera observado nos testes ficou em 4 minutos no minuto zero, zerando após 20 minutos.
Decisões e trade-offs
Cada escolha principal carrega um custo aceito de forma explícita, e a tabela registra esse balanço.
| Decisão | Alternativa descartada | Trade-off aceito |
|---|---|---|
| Carrinho só em Redis com TTL | Carrinho relacional por usuário | Falha grave do Redis apaga carrinhos anônimos em andamento; mitigado por réplica e AOF |
| Reserva de estoque na transação do pedido | Reserva assíncrona por evento | Contenção no estoque de itens campeões; mitigada por fila de espera por sku |
| Outbox + Kafka para integração | Chamada síncrona entre serviços | Consistência eventual entre domínios; telas exibem estado em progresso |
| Idempotência por constraint única no banco | Lock distribuído no Redis | Escrita extra a cada pedido; ganha garantia durável e auditável |
| Waiting room na abertura da campanha | Escalar sem limite para o pico | Parte dos clientes espera minutos na fila; o checkout interno permanece estável |
| Cache write-through no catálogo | Read-through com reconstrução por leitura | Custo de escrita a cada mudança de preço; elimina thundering herd após invalidação |
| Particionamento mensal de pedidos | Tabela única com índices | Custo de gestão de partições; consultas por período e retenção ficam triviais |
O fio condutor dessas decisões é preferir falha legível a comportamento mágico. O rollback por estoque insuficiente é entendível pelo cliente; um oversell silencioso não é. A espera na fila é comunicada com posição estimada; uma indisponibilidade total não é. Nenhuma decisão busca eliminar a falha, todas buscam controlar seu raio e sua mensagem.
Escalabilidade e falhas
A escala de leitura é resolvida antes da origem. A CDN serve páginas de produto com TTL de 60 segundos e invalidação por evento de preço, derrubando a origem para 2.400 requisições por segundo mesmo no pico de 12.000. O cache de catálogo em Redis mantém hit ratio acima de 90% com política allkeys-lru, e a falha do cache degrada para o banco, que suporta a carga residual com réplicas de leitura. Mediu-se que a degradação sem cache eleva o p99 de produto de 150 para 400 ms, acima do alvo, porém sem indisponibilidade, e esse é o compromisso aceito.
A escala de escrita usa target tracking por requisição. O serviço de pedidos roda em ECS Fargate com política de auto scaling por requisições por tarefa, mínimo de 12 e máximo de 120 instâncias. Como o pico da Black Friday é conhecido com dias de antecedência, a plataforma aplica pré-escala agendada 30 minutos antes da abertura, eliminando o atraso reativo de 5 a 8 minutos do scale-out baseado em métrica.
# Política de auto scaling do serviço de pedidos (ECS)
policy:
type: TargetTrackingScaling
target_tracking:
predefined_metric: ALBRequestCountPerTarget # requisições por tarefa
target_value: 500 # alvo por tarefa por minuto
min_capacity: 12 # piso para absorver falha de zona
max_capacity: 120 # teto como guardrail de custo
scale_in_cooldown: 900 # 15 min para reduzir sem oscilar
As falhas têm resposta prevista e ensaiada. O circuit breaker do antifraude abre com 20% de falhas em 30 segundos e cai para um fluxo de revisão manual, mantendo o checkout vivo com aprovação postergada. Falha da adquirente aciona retry com a mesma chave de idempotência e, persistindo, roteamento para a adquirente secundária. Eventos que falham na entrega vão para dead letter queue com replay controlado, e todos os consumidores são idempotentes porque a reentrega é tratada como regra, não como exceção.
Durante a campanha, o painel central acompanha o funil e não apenas os serviços. Conversão por etapa, tempo de resposta do antifraude, lag do consumidor de eventos e fila do waiting room são as quatro séries na primeira linha do painel, com alertas diretos ao plantão. Um lag acima de 30 segundos no consumidor de expedição, por exemplo, dispara ação antes que clientes percebam pedido sem rastreio.
Lições aplicáveis
- Separe leitura quente de escrita crítica: catálogo e vitrine suportam cache agressivo, enquanto pedido e pagamento exigem transação e não devem competir com leitura por capacidade.
- Idempotência é contrato de fronteira, não detalhe de implementação: sem ela, todo retry de rede se transforma em cobrança duplicada, e o pico multiplica o problema.
- Pré-escala vence reação: quando o pico é conhecido, agendar capacidade elimina o intervalo em que a métrica sobe mais rápido que o autoscaler reage.
- Todo efeito colateral precisa de compensação declarada: reserva de estoque, e-mail e cupom nascem com o caminho de desfazer definido junto.
- Eventos amortecem picos e criam histórico: o outbox com Kafka transformou picos de escrita em consumo regulado e alimentou analytics sem instrumentação adicional.
- Falha legível supera comportamento mágico: mensagens claras de espera e indisponibilidade reduziram abertura de chamados em 40% nos testes de carga.
- Meça o funil, não só o serviço: indisponibilidade técnica nem sempre é a maior perda de conversão; nos testes, a lentidão do antifraude custou mais pedidos que qualquer queda de servidor.
- Ensaiar o pico antes dele: o teste de carga com perfil real de abertura de campanha revelou gargalo no pool de conexões do PostgreSQL, corrigido duas semanas antes da Black Friday.