DevGuideTK
← Trilha Backend

Idempotência

Como tornar seguras as operações repetidas quando a rede falha no meio do pedido.

O problema: o cliente não sabe se deu certo

Um aplicativo envia um pagamento de R$ 159,90. O servidor recebe, cobra o cartão, grava o pagamento e começa a responder. Nesse instante, o celular troca de rede, e a resposta se perde.

Do lado do aplicativo, a chamada falhou com tempo esgotado. Ele não tem como saber se o servidor:

  • nem recebeu o pedido;
  • recebeu e falhou antes de cobrar;
  • cobrou, gravou, e só a resposta se perdeu.

A reação natural, do aplicativo ou da pessoa apertando o botão de novo, é tentar outra vez. Se o servidor tratar a nova tentativa como um pagamento novo, o cliente é cobrado duas vezes.

O mesmo acontece entre serviços: bibliotecas de cliente HTTP, proxies e filas de mensagens repetem pedidos automaticamente quando não recebem resposta, como mostram os tópicos de HTTP e de filas.

O que é idempotência

Uma operação é idempotente quando fazê-la uma vez ou várias vezes deixa o sistema no mesmo estado.

OperaçãoIdempotente?Por quê
GET /pedidos/981SimSó lê
PUT /clientes/42 com o cadastro completoSimGravar o mesmo conteúdo duas vezes dá o mesmo resultado
DELETE /pedidos/981SimDepois da primeira vez, o pedido já não existe; repetir não muda nada
POST /pagamentosNãoCada chamada cria um pagamento novo
POST /pedidos/981/cancelamentoDepende de como é feitoCancelar um pedido já cancelado pode ser ignorado, ou dar erro

O problema está quase sempre no POST, que é justamente o método usado para criar coisas: pedidos, pagamentos, envios, reservas.

A chave de idempotência

A solução mais usada em APIs, popularizada por serviços de pagamento como a Stripe, é a chave de idempotência: o cliente gera um identificador único para cada operação e o envia num cabeçalho. Se precisar repetir, envia a mesma chave.

POST /pagamentos HTTP/1.1
Idempotency-Key: 7f3c9a2e-4b1d-4e8a-9c3f-2d6b8e1a5f40
Content-Type: application/json

{ "pedidoId": 981, "valor": 15990 }

O servidor guarda cada chave com a resposta que deu:

SituaçãoO que o servidor faz
Chave nunca vistaProcessa o pedido, guarda a chave e a resposta, responde
Chave já vista, mesmo conteúdoNão processa de novo; devolve a resposta guardada
Chave já vista, conteúdo diferenteRecusa: a chave está sendo reutilizada para outra operação
Chave em processamento por outro pedidoEspera terminar e devolve a mesma resposta, ou responde 409 para o cliente tentar depois

O ponto importante é que a chave identifica a intenção do cliente, e não o conteúdo. Dois pagamentos de R$ 159,90 para o mesmo pedido, feitos de propósito, têm chaves diferentes. Uma nova tentativa do mesmo pagamento tem a mesma chave.

Por que conferir antes não basta

A implementação mais intuitiva é conferir se a chave já existe e, se não existir, processar:

1. SELECT: a chave existe?
2. se não existe: cobrar, gravar o pagamento, gravar a chave

Testamos essa versão disparando 20 pedidos simultâneos com a mesma chave contra um PostgreSQL, como acontece quando um cliente insiste ou várias camadas tentam de novo ao mesmo tempo. Resultado: 20 pagamentos. Os 20 pedidos fizeram a conferência antes que qualquer um tivesse gravado a chave, e os 20 seguiram em frente.

É o mesmo problema da atualização perdida, visto no tópico de transações: "conferir e depois agir" deixa uma janela entre os dois passos.

A versão que funciona

A conferência e a reserva da chave precisam ser uma única operação atômica. Com um banco relacional, a forma mais simples é uma restrição única na chave e um INSERT ... ON CONFLICT DO NOTHING, na mesma transação do efeito:

  1. Abrir a transação e tentar inserir a chave.
  2. Se a inserção funcionou, esta é a primeira vez: criar o pagamento e guardar a resposta junto com a chave. Confirmar.
  3. Se a chave já existia, desfazer e devolver a resposta guardada.

O detalhe que faz isso funcionar com pedidos simultâneos: quando duas transações tentam inserir a mesma chave, a segunda espera a primeira terminar. Se a primeira confirmar, a segunda encontra a chave pronta, com a resposta guardada. Se a primeira falhar e desfizer tudo, a segunda segue e processa normalmente.

Testamos essa versão nas cinco linguagens, cada uma com 20 pedidos simultâneos com a mesma chave e, depois, um pedido com a mesma chave e outro valor:

LinguagemPagamentos criadosRespostas dos 20 pedidosMesma chave, outro conteúdo
TypeScript1Todas 201, com o mesmo pagamento422
Python1Todas 201, com o mesmo pagamento422
Go1Todas 201, com o mesmo pagamento422
Java1Todas 201, com o mesmo pagamento422
C#1Todas 201, com o mesmo pagamento422

Idempotência sem chave

Nem toda operação precisa de uma chave enviada pelo cliente. Às vezes a idempotência vem do próprio desenho:

TécnicaExemplo
Identificador natural únicoUma fatura por pedido: UNIQUE (pedido_id) impede a segunda
Definir em vez de somarUPDATE pedidos SET status = 'pago' repetido não muda nada; saldo = saldo - 100 repetido muda
Identificador gerado pelo clienteO cliente cria o identificador do recurso e usa PUT /pedidos/{id}, que é idempotente
Registro de mensagens processadasConsumidores de filas guardam o identificador de cada mensagem, como no tópico de filas

Por que isso importa quando a IA escreve o código

Um assistente escreve a rota de pagamento ou de criação de pedido para um pedido por vez:

  • Nenhuma proteção contra repetição em POST que criam ou cobram.
  • A chave de idempotência conferida com um SELECT antes de gravar, o que falha com pedidos simultâneos.
  • A chave guardada sem a resposta, devolvendo 409 ou um erro numa repetição legítima.
  • A chave aceita com qualquer conteúdo, permitindo que um erro do cliente reaproveite a chave para outra operação.
  • Novas tentativas automáticas no cliente sem chave, transformando cada tempo esgotado em cobrança dupla.

O próximo nível compara as formas de guardar as chaves e de lidar com efeitos externos, como a chamada a um provedor de pagamento.

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Idempotência em cinco minutos

    Explique em no máximo cinco minutos de leitura o que é idempotência em APIs, para alguém que nunca pensou no que acontece quando um pedido é repetido.
    
    Cubra só o essencial: por que o cliente repete pedidos, quais métodos HTTP já são idempotentes, o que é uma chave de idempotência e por que conferir antes de gravar não funciona com pedidos simultâneos.
    
    Use o exemplo de um pagamento. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
  • Quando o cliente não sabe se deu certo

    Quero entender o problema que a idempotência resolve.
    
    Aja como um mentor. Monte comigo uma linha do tempo de um pagamento em que a resposta se perde depois que o servidor já cobrou.
    
    Conduza assim:
    1. Peça que eu liste o que o cliente sabe e o que não sabe naquele momento.
    2. Pergunte o que acontece se ele tentar de novo sem nenhuma proteção.
    3. Explique quem mais repete pedidos automaticamente: bibliotecas de cliente, proxies, filas, pessoas.
    4. Explique a definição de idempotência e quais métodos HTTP já a garantem.
    5. Apresente a chave de idempotência e o que o servidor faz com ela em cada situação.
    
    Faça uma pergunta de verificação em cada etapa.
  • Por que conferir antes de gravar falha

    Quero entender por que a implementação "conferir se a chave existe e depois gravar" falha.
    
    Monte uma linha do tempo com dois pedidos simultâneos com a mesma chave e mostre, passo a passo, como os dois passam pela conferência.
    
    Depois explique:
    1. A relação com a atualização perdida em transações de banco.
    2. Como uma restrição única e um INSERT com ON CONFLICT tornam a reserva atômica.
    3. Por que a segunda transação espera a primeira, e o que acontece se a primeira falhar.
    4. Por que guardar a chave no Redis e o efeito no banco deixa uma janela de inconsistência.
    
    Me faça três perguntas de cenário e corrija as minhas respostas.
  • Idempotência sem chave

    Quero aprender a desenhar operações que são idempotentes por natureza, sem precisar de uma chave enviada pelo cliente.
    
    Explique, com exemplos de SQL e de rotas:
    1. Identificadores naturais únicos, como uma fatura por pedido.
    2. Definir valores em vez de somar.
    3. Identificadores gerados pelo cliente com PUT.
    4. Registro de mensagens processadas em consumidores de filas.
    
    Depois me mostre seis operações e pergunte se cada uma é idempotente e, se não for, como torná-la. Corrija as minhas respostas.