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ção | Idempotente? | Por quê |
|---|---|---|
GET /pedidos/981 | Sim | Só lê |
PUT /clientes/42 com o cadastro completo | Sim | Gravar o mesmo conteúdo duas vezes dá o mesmo resultado |
DELETE /pedidos/981 | Sim | Depois da primeira vez, o pedido já não existe; repetir não muda nada |
POST /pagamentos | Não | Cada chamada cria um pagamento novo |
POST /pedidos/981/cancelamento | Depende de como é feito | Cancelar 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ção | O que o servidor faz |
|---|---|
| Chave nunca vista | Processa o pedido, guarda a chave e a resposta, responde |
| Chave já vista, mesmo conteúdo | Não processa de novo; devolve a resposta guardada |
| Chave já vista, conteúdo diferente | Recusa: a chave está sendo reutilizada para outra operação |
| Chave em processamento por outro pedido | Espera 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 chaveTestamos 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:
- Abrir a transação e tentar inserir a chave.
- Se a inserção funcionou, esta é a primeira vez: criar o pagamento e guardar a resposta junto com a chave. Confirmar.
- 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:
| Linguagem | Pagamentos criados | Respostas dos 20 pedidos | Mesma chave, outro conteúdo |
|---|---|---|---|
| TypeScript | 1 | Todas 201, com o mesmo pagamento | 422 |
| Python | 1 | Todas 201, com o mesmo pagamento | 422 |
| Go | 1 | Todas 201, com o mesmo pagamento | 422 |
| Java | 1 | Todas 201, com o mesmo pagamento | 422 |
| C# | 1 | Todas 201, com o mesmo pagamento | 422 |
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écnica | Exemplo |
|---|---|
| Identificador natural único | Uma fatura por pedido: UNIQUE (pedido_id) impede a segunda |
| Definir em vez de somar | UPDATE pedidos SET status = 'pago' repetido não muda nada; saldo = saldo - 100 repetido muda |
| Identificador gerado pelo cliente | O cliente cria o identificador do recurso e usa PUT /pedidos/{id}, que é idempotente |
| Registro de mensagens processadas | Consumidores 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
POSTque criam ou cobram. - A chave de idempotência conferida com um
SELECTantes de gravar, o que falha com pedidos simultâneos. - A chave guardada sem a resposta, devolvendo
409ou 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.