Pular para o conteúdo

Um caso técnico completo: reserva de estoque com concorrência

Condições de corrida, chaves de idempotência e outbox transacional — simulados passo a passo.

⏱ 10 minCapítulo 11 / 18

11.1O trabalho começa pelo contrato

Nomeie os invariantes antes de delegar

“Crie um endpoint de reserva” deixa decisões importantes em aberto. O engenheiro identifica primeiro o que deve permanecer verdadeiro — inclusive sob falha — e resolve questões de domínio, como expiração, com quem é responsável por elas.

  1. I1A quantidade disponível nunca fica negativa.
  2. I2Dentro da janela de retenção da chave, repetir a mesma intenção produz uma única reserva lógica.
  3. I3Reutilizar a chave com conteúdo diferente gera um resultado explicitamente definido.
  4. I4Um cliente só atua sobre seus próprios dados.
  5. I5Uma falha parcial nunca produz reserva sem evento rastreável.
  6. I6O resultado pode ser descoberto quando a conexão cai depois do commit.
contrato-reserva.yaml
objetivo: "Reservar unidades de um item para o cliente autenticado"escopo:  inclui: [API de reserva, persistencia, evento, testes, telemetria]  exclui: [pagamento, alteracao do fluxo de expedicao]invariantes:  - "disponibilidade >= 0"  - "uma reserva logica por cliente e chave durante a janela de retencao"  - "mesma chave com payload diferente produz conflito"  - "nenhum acesso a dados de outro cliente"evidencias:  - "teste com requisicoes concorrentes disputando o ultimo item"  - "teste de repeticao apos commit e antes da resposta"  - "teste de isolamento entre clientes"  - "teste de recuperacao da publicacao do evento"decisoes_pendentes:  - "politica de expiracao da reserva e da chave"

O contrato só estará pronto para implementação completa depois de resolver as pendências que afetam a semântica.

11.2A fronteira de atomicidade

Duas requisições, uma última unidade

Uma solução ingênua lê a disponibilidade e depois grava a redução: duas requisições podem agir sobre uma premissa que deixou de ser verdadeira. Avance pelos dois desenhos.

Disputa pela última unidade

Requisição A
linha de estoque
Requisição B

Estado compartilhado

disponível
1
bloqueio da linha
—
reservas concedidas
0

Aperte reproduzir ou avance passo a passo.

Ler, depois gravar

update-condicional.sql
-- Fragmento ilustrativo: quantidade deve ser validada como inteira positiva.-- A identidade do cliente vem da autenticacao, nao do corpo da requisicao.UPDATE inventorySET available = available - :quantityWHERE tenant_id = :authenticated_tenant  AND item_id = :item_id  AND available >= :quantityRETURNING available;

11.3Falha parcial

Gravar a reserva e publicar o evento são duas operações

Se uma acontece e a outra falha, os sistemas divergem. A outbox transacional grava o evento na mesma transação e o publica depois. A entrega pode repetir; consumidores precisam lidar com duplicatas. [F30]

Banco + broker

Serviço de reservas
Banco
Broker → consumidor

Estado compartilhado

reserva gravada
sim
evento na outbox
—
efeitos no consumidor
0

Aperte reproduzir ou avance passo a passo.

Escrita dupla

11.4Dividindo o trabalho

Frentes com interfaces definidas

Essas frentes podem ser pessoas, agentes ou ambos — em paralelo só onde as dependências permitem. A investigação inicial pode revelar uma decisão de domínio que precisa vir antes.

11.5Evidência além do caminho feliz

Os sete cenários que o engenheiro pede

Mocks não demonstram bloqueio e isolamento. Coordene a sequência relevante com barreiras — apenas lançar requisições juntas pode não reproduzir a disputa. Quando viável, uma versão deliberadamente vulnerável prova que o teste detecta o defeito. Marque o que seus testes cobrem.

0 / 7

11.6Implantável e observável

Entregue com sinais e condições de interrupção

A entrega inclui a alteração, a migração compatível, os testes e os sinais de operação — com evidência vinculada à versão validada. Numa implantação gradual, acompanhe conflitos de reserva, latência, erros, atrasos na outbox e divergências de invariantes, com condições de interrupção definidas antes da exposição. Reverter o binário não desfaz uma mutação de dados já persistida. [F33]

A maior contribuição humana foi intenção, concorrência, falha parcial, contratos, evidência independente e operação. A implementação foi um campo de execução delegável dentro dessas condições.

Pontos-chave

  1. 1

    Comece pelos invariantes e resolva as decisões de domínio que mudam a semântica.

  2. 2

    Coloque verificação e efeito na mesma operação atômica; ler-depois-gravar gera corrida.

  3. 3

    Combine chaves de idempotência, outbox transacional e deduplicação no consumidor para falhas parciais.

  4. 4

    Exija evidência além do caminho feliz com banco real e concorrência coordenada — e registre o que aprendeu.

Verifique seu entendimento

Pergunta 1 de 3

Por que uma reserva do tipo ler-depois-gravar pode vender duas vezes a última unidade?

Ler a seção completa do relatório originalO texto integral — com todas as fontes e nuances.

Cenário ilustrativo criado para este relatório. O objetivo é mostrar como as competências se combinam; os exemplos não constituem uma implementação pronta para produção.

Uma aplicação precisa permitir que um cliente reserve unidades de um item. Existem múltiplos clientes organizacionais, requisições simultâneas, retries após timeout e um evento que informa a criação da reserva a outro serviço.

11.1. O trabalho começa pelo contrato

Pedir “crie um endpoint de reserva” deixa decisões importantes em aberto. Antes de delegar, o engenheiro identifica os invariantes:

  • A quantidade disponível nunca pode ficar negativa.
  • Durante a janela contratada de retenção da chave, repetir a mesma intenção deve produzir uma única reserva lógica.
  • Reutilizar a mesma chave com conteúdo diferente precisa gerar um resultado explicitamente definido.
  • Um cliente só pode atuar sobre seus próprios dados.
  • Uma falha parcial não pode produzir reserva sem rastreabilidade do evento correspondente.
  • O sistema deve permitir descobrir o resultado quando a conexão cai depois da confirmação no banco.

O engenheiro também resolve com o domínio se a reserva expira, como é cancelada e por quanto tempo a chave de idempotência continua válida. O contrato deve dizer o que ocorre quando a chave expira e quando a reserva expira antes dela: recuperar a operação original ou iniciar outra intenção com uma nova chave são comportamentos distintos. Essas decisões alteram a modelagem.

Um contrato resumido poderia ser:

objetivo: "Reservar unidades de um item para o cliente autenticado"
escopo:
  inclui: [API de reserva, persistencia, evento, testes, telemetria]
  exclui: [pagamento, alteracao do fluxo de expedicao]
invariantes:
  - "disponibilidade >= 0"
  - "uma reserva logica por cliente e chave durante a janela de retencao"
  - "mesma chave com payload diferente produz conflito"
  - "nenhum acesso a dados de outro cliente"
evidencias:
  - "teste com requisicoes concorrentes disputando o ultimo item"
  - "teste de repeticao apos commit e antes da resposta"
  - "teste de isolamento entre clientes"
  - "teste de recuperacao da publicacao do evento"
decisoes_pendentes:
  - "politica de expiracao da reserva e da chave"

Os campos são um modelo ilustrativo. O contrato só estará pronto para implementação completa depois de resolver as pendências que afetam a semântica.

11.2. O engenheiro define a fronteira de atomicidade

Uma solução ingênua lê a disponibilidade e depois grava a redução. Duas requisições podem observar o mesmo estoque e agir sobre uma premissa que deixou de ser verdadeira.

Para um item representado por uma única linha, uma operação condicional pode ser parte da solução:

-- Fragmento ilustrativo: quantidade deve ser validada como inteira positiva.
-- A identidade do cliente vem da autenticacao, nao do corpo da requisicao.
UPDATE inventory
SET available = available - :quantity
WHERE tenant_id = :authenticated_tenant
  AND item_id = :item_id
  AND available >= :quantity
RETURNING available;

No PostgreSQL, a semântica de atualização concorrente em Read Committed inclui reavaliar a condição sobre a versão atualizada da linha após aguardar um concorrente. É essa propriedade documentada, junto das restrições do esquema, que deve fundamentar o raciocínio; o formato visual da consulta não prova sua adequação. F28

O fragmento não resolve sozinho idempotência, criação da reserva, autorização completa, expiração ou publicação de eventos. Um desenho possível garante uma única linha de estoque por (tenant_id, item_id) e usa uma restrição única para (tenant_id, idempotency_key) no registro de operações. A coordenação da chave, o decremento condicional, a reserva, o resultado recuperável e a inserção na outbox participam da mesma transação.

O caminho de repetição consulta o resultado confirmado e compara os parâmetros relevantes. O tratamento de concorrência precisa distinguir conflito, operação ainda não confirmada e resultado concluído, sem permitir um segundo efeito. O contrato também define se recusas, como estoque insuficiente, ficam associadas à chave ou permitem uma nova tentativa. Essa é uma proposta de desenho para o exemplo, cuja implementação exige validação própria.

O contrato de repetição também precisa distinguir a mesma intenção de outra operação com parâmetros iguais. A abordagem de identificadores explícitos apresentada pela AWS ajuda a formular esse aspecto. F29

11.3. O evento exige tratamento de falha parcial

Gravar a reserva e publicar diretamente em um broker são duas operações distintas. Se uma acontecer e a outra falhar, pode haver divergência.

Uma possibilidade é gravar um evento em uma outbox na mesma transação da reserva e usar um publicador para entregá-lo posteriormente. Esse padrão trata a fronteira entre persistência e publicação. A entrega pode repetir mensagens; consumidores precisam lidar com duplicatas conforme o contrato. A documentação da AWS descreve o padrão e suas condições. F30

O engenheiro decide se esse desenho corresponde aos requisitos e define como observar atrasos, retentar publicação e reconhecer eventos processados. A IA pode implementar cada componente e produzir alternativas. A coerência do contrato entre eles permanece uma decisão de engenharia.

Para o exemplo, cada evento tem um identificador estável. O consumidor pode registrar esse identificador e aplicar seu efeito na mesma transação local, ou executar uma operação cujo contrato seja idempotente. Se o efeito ocorre em outro serviço, a garantia depende também desse serviço. A outbox não fornece, sozinha, efeito único no destino.

11.4. O trabalho é dividido com interfaces já definidas

Uma organização possível seria:

FrenteEntregaCondição de integração
InvestigaçãoMapa do código existente e das regras de estoqueReferências verificadas a arquivos e comportamento atual.
ImplementaçãoAlteração de API e persistênciaContrato de idempotência e esquema acordados.
VerificaçãoCenários de concorrência, repetição e isolamentoCritérios derivados dos invariantes e uso de banco real.
OperaçãoTelemetria, migração e plano de implantaçãoCompatibilidade com o ambiente e sinais de falha definidos.

Essas frentes podem ser pessoas, agentes ou combinações. A divisão só deve ser paralela onde as dependências permitirem. A investigação inicial pode revelar que uma decisão de domínio precisa ser resolvida antes de qualquer implementação.

11.5. A evidência vai além do caminho feliz

O engenheiro pede resultados observáveis para cenários específicos:

  1. Duas requisições disputam a última unidade: apenas uma obtém a reserva.
  2. Duas requisições usam a mesma chave: há um único efeito no estoque.
  3. A mesma chave reaparece com outra quantidade: ocorre o conflito especificado.
  4. O processo termina depois do commit e antes da resposta: a repetição recupera o resultado definido.
  5. Um cliente tenta referenciar o item de outro: não há exposição nem mutação indevida.
  6. O broker aceita o evento, mas o publicador termina antes de registrar a publicação: a retomada pode repetir a mensagem; a deduplicação ou idempotência do consumidor evita um novo efeito.
  7. A reserva expira durante operações concorrentes: a regra acordada preserva consistência.

Mocks isolados podem auxiliar testes locais, mas não demonstram o comportamento de bloqueio e isolamento do banco. Os testes pertinentes devem exercitar esse comportamento, coordenando a sequência relevante de operações com barreiras ou pontos de sincronização. Apenas lançar requisições juntas pode deixar de reproduzir a disputa desejada. Quando viável, introduzir uma versão deliberadamente vulnerável à corrida permite confirmar que o teste detecta o defeito pretendido.

11.6. O resultado precisa ser implantável e observável

A entrega inclui a alteração, a migração compatível, os testes pertinentes e os sinais de operação. A evidência deve identificar a versão validada. Se a integração com outra mudança alterar o comportamento, a validação correspondente precisa ser repetida sobre o resultado integrado.

Para uma implantação gradual, podem ser acompanhados conflitos de reserva, latência, erros, atrasos na outbox e divergências de invariantes. As condições de interrupção devem ser definidas antes da exposição. Mudanças de dados exigem cuidado próprio: reverter o binário não desfaz automaticamente uma mutação já persistida.

O capítulo sobre canarying do Google SRE oferece uma referência para combinar automação de releases, mudanças pequenas e avaliação de uma versão antes de ampliar sua exposição. F33

11.7. Onde esteve a competência do engenheiro

A maior contribuição humana esteve em definir a intenção, reconhecer concorrência e falhas parciais, escolher os contratos, separar frentes, exigir evidência independente e preparar operação. A implementação foi um campo de execução delegável dentro dessas condições.

Ao terminar, o engenheiro registra os aprendizados úteis: um exemplo de teste de concorrência, a política de idempotência, um procedimento de migração e os sinais de diagnóstico. A próxima tarefa semelhante começa de uma posição melhor.