Imagine que um cliente clique duas vezes no botão “Finalizar pagamento” em um aplicativo de delivery ou ecommerce. Ou que a rede falhe logo após o envio de uma requisição de checkout, mas antes da confirmação para o cliente. Como garantir que o sistema não processe a mesma compra duas vezes?
A resposta está em um conceito fundamental para sistemas financeiros e distribuídos: idempotência.
Neste artigo, vamos explorar como empresas como Stripe, Uber e outras implementam APIs idempotentes para garantir que operações críticas sejam executadas uma e apenas uma vez.
O problema da duplicidade
Requisições HTTP podem ser reexecutadas por inúmeras razões:
- O cliente reenvia manualmente a requisição.
- O frontend ou SDK faz retry automático.
- O servidor retorna timeout ou erro intermitente, mas processou a operação.
- Algum proxy intermediário reenvia o pacote.
Em operações como criação de pedidos, cobranças, transferências bancárias ou reservas, isso pode ser catastrófico: gerar cobranças duplicadas, inventários inconsistentes ou clientes frustrados.
Para entender melhor a motivação por trás da idempotência e como ela se relaciona com o conceito de operações seguras em sistemas distribuídos, recomendamos também o artigo: 👉 Idempotência em software: conceitos, importância e aplicações
Idempotência na prática: 10 técnicas essenciais
Vamos destrinchar as técnicas usadas por empresas que lidam com alto volume de operações críticas, como Stripe e Shopify:
1. APIs idempotentes por design
Projetar uma API idempotente não é apenas adicionar uma verificação de chave. Exige pensar no fluxo como um todo: onde há efeitos colaterais? O que acontece se a operação for repetida? Por exemplo, se sua API cria uma cobrança e envia um e-mail, você precisa garantir que o e-mail não será disparado novamente num retry. Isso exige separar ações idempotentes de não-idempotentes ou adotar mecanismos de compensação.
2. Uso de UUIDs como idempotency key
A escolha de UUID como chave vem do seu alto grau de entropia e unicidade. Ao contrário de hashes baseados em payload, UUIDs não dependem do conteúdo da requisição, o que os torna mais versáteis — e menos propensos a colisões por mudanças sutis de payload. Um UUID pode ser criado no frontend, no mobile ou em edge servers com total segurança.
3. Envio da chave via HTTP Header
Padronizar o header Idempotency-Key
facilita a interceptação por middlewares, logging e até ferramentas de observabilidade. Também evita que a chave se perca em cache de requisições GET e evita exposição em logs de URL. Alguns proxies e gateways, como API Gateway da AWS ou Apigee, já reconhecem e propagam esses headers de forma nativa.
4. Chave nova para payload novo
Reutilizar o mesmo UUID para dois conteúdos diferentes quebra o modelo de idempotência. Isso pode gerar retornos inconsistentes, falhas de integridade ou respostas não confiáveis. Uma prática recomendada é atrelar a geração do UUID à mudança de conteúdo da operação: novo carrinho, nova cobrança, nova transação. Frontends modernos automatizam isso com hooks de formulário ou stores reativos.
5. Armazenamento da chave em banco
Persistir a chave com o payload de resposta (e eventualmente status da requisição) permite que o sistema reconheça repetições. Essa base deve ter características de durabilidade, baixa latência e alta consistência. Redis pode ser usado com TTLs para baixa criticidade, mas para sistemas financeiros, bancos relacionais transacionais (PostgreSQL, MySQL, etc.) são preferíveis.
6. Consulta antes de processar
A verificação deve ser a primeira operação executada. Não adianta executar lógica de negócio e só depois consultar a chave. Esse padrão exige disciplina na arquitetura: middlewares, filters ou interceptadores podem garantir que esse passo seja uniforme. Também pode ser necessário replicar a verificação entre múltiplas instâncias da aplicação.
7. Processa apenas se for novo
A atomicidade aqui é crítica: entre o tempo de checar se a chave existe e gravá-la, o sistema pode estar concorrendo com outra instância. O ideal é que a verificação e gravação ocorram em uma operação única e atômica — como um INSERT ... ON CONFLICT DO NOTHING
ou SELECT FOR UPDATE
com lock pessimista.
8. Rollback em caso de erro interno
Falhas em middlewares, validações tardias ou na persistência final podem deixar a operação pela metade. Técnicas como unit of work, transações distribuídas ou uso de outbox pattern com mensageria confiável (Kafka, SQS) ajudam a preservar a consistência. Em bancos que suportam ACID completo, rollback pode ser automático — mas cuidado com dependências externas como gateways de pagamento.
9. Expiração da chave
Guardar todas as chaves para sempre não é escalável. TTLs ajudam a equilibrar uso de disco e segurança contra reexecução. O tempo ideal depende do SLA de repetição: se clientes podem reexecutar em até 24h, esse deve ser o TTL. Além disso, logs, métricas e dashboards devem sinalizar usos inválidos de chave repetida.
10. Retry com backoff exponencial e jitter
A prática de retries precisa ser feita com cuidado. Sem jitter, múltiplos clientes disparam novas requisições ao mesmo tempo — criando picos. Com jitter, os intervalos se tornam irregulares, evitando sobrecarga. Em sistemas assíncronos, pode-se usar filas com delay progressivo, e em sistemas síncronos, bibliotecas de retry com backoff adaptativo.
Para uma visão mais ampla sobre como lidar com sistemas sob pressão, leia: 👉 Processamento assíncrono: os desafios da escalabilidade
Exemplo real simplificado (pseudocódigo)
[HttpPost("/checkout")]
public IActionResult Checkout([FromHeader(Name = "Idempotency-Key")] string idemKey, OrderRequest body)
{
var result = idemRepo.Find(idemKey);
if (result != null) return Ok(result);
using var tx = db.BeginTransaction();
var order = OrderService.Create(body);
PaymentGateway.Charge(order);
idemRepo.Save(idemKey, order);
tx.Commit();
return Ok(order);
}
Esse exemplo demonstra a ordem correta das operações: validação da chave, execução protegida por transação e persistência do resultado.
Conclusão
Idempotência não é um recurso de luxo: é uma exigência para sistemas confiáveis em ambiente distribuído. Implementar corretamente esse padrão é garantir que cada intenção do usuário seja tratada de forma única, previsível e segura.
E, como vimos, com boas práticas e padrões simples, você pode blindar seu sistema contra os efeitos colaterais de retries, timeouts e falhas de comunicação.
Quer ir além da idempotência?
Conheça outros padrões de resiliência como bulkhead e back-pressure no artigo: Estratégias de resiliência para microservices