Idempotência

Como projetar operações que podem ser executadas várias vezes sem efeitos colaterais duplicados, essencial para retentativas seguras e processamento confiável de eventos.

Que problema resolve

Em sistemas distribuídos, as mensagens e as requisições podem chegar mais de uma vez por diversas razões:

  • O cliente tenta novamente uma requisição porque não recebeu resposta (mas o servidor a processou)
  • O broker de mensageria entrega um evento duplicado (garantia at-least-once)
  • Uma falha de rede faz com que a confirmação se perca, e o emissor reenvia
  • O usuário dá um clique duplo no botão de “Pagar”
Sem idempotência:
  Cliente ──► POST /payments {amount: 100}
              ✅ Pagamento processado (mas a resposta se perde)
  
  Cliente ──► POST /payments {amount: 100}  (retentativa)
              ✅ Pagamento processado DE NOVO
  
  Resultado: Foram cobrados $200 em vez de $100

Sem idempotência, as retentativas e os duplicados causam efeitos colaterais repetidos: cobranças em dobro, estoque descontado duas vezes, e-mails duplicados, registros duplicados no banco de dados.

Como funciona

Uma operação é idempotente quando executá-la uma vez produz o mesmo resultado que executá-la várias vezes. O sistema detecta requisições duplicadas e devolve o resultado original sem executar a operação novamente.

Com idempotência:
  Cliente ──► POST /payments
              Idempotency-Key: "PAY-abc-123"
              {amount: 100}
              ✅ Pagamento processado → a resposta se perde
  
  Cliente ──► POST /payments
              Idempotency-Key: "PAY-abc-123"  (mesma key)
              {amount: 100}
              ✅ Devolve o resultado anterior (sem processar de novo)
  
  Resultado: Foi cobrado $100 uma única vez

Mecanismo com Idempotency Key

A abordagem mais comum é usar uma chave de idempotência (idempotency key) que identifica de forma única cada operação:

1. Cliente gera um ID único: "PAY-abc-123"
2. Cliente envia a request com o header Idempotency-Key
3. Servidor verifica se já processou essa key:
   a. Se NÃO existe → processa a operação, salva o resultado com a key
   b. Se SIM existe → devolve o resultado salvo sem reprocessar

Armazenamento de keys

ArmazenamentoVantagensDesvantagens
Banco de dadosPersistente, transacionalMais lento, requer limpeza
Redis/CacheRápido, TTL automáticoPode perder dados se reiniciar
Tabela dedicadaSeparação clara, fácil de consultarTabela adicional para manter

Exemplo de fluxo completo

Tabela: idempotency_keys
┌──────────────────┬────────┬──────────────────┬─────────┐
│ key              │ status │ response         │ expires │
├──────────────────┼────────┼──────────────────┼─────────┤
│ PAY-abc-123      │ done   │ {paymentId: 456} │ 24h     │
│ PAY-def-456      │ doing  │ null             │ 24h     │
└──────────────────┴────────┴──────────────────┴─────────┘

Request 1: POST /payments (key: PAY-abc-123)
  → Key não existe → INSERT key com status "doing"
  → Processar pagamento → UPDATE key com status "done" e response
  → Devolver response

Request 2: POST /payments (key: PAY-abc-123) [retentativa]
  → Key existe com status "done"
  → Devolver response salva (sem reprocessar)

Request 3: POST /payments (key: PAY-def-456)
  → Key existe com status "doing" (em processamento)
  → Devolver HTTP 409 Conflict (aguarde e tente novamente)

Idempotência em consumidores de eventos

Para consumidores de eventos, a idempotência é implementada rastreando quais eventos já foram processados:

Evento recebido: OrderCreated (eventId: "EVT-789")

1. Verificar se o eventId já foi processado
   → SELECT * FROM processed_events WHERE event_id = 'EVT-789'
   
2a. Se NÃO existe:
   → Processar evento (reservar estoque)
   → INSERT INTO processed_events (event_id, processed_at)
   → ACK ao broker
   
2b. Se SIM existe:
   → Ignorar (já foi processado)
   → ACK ao broker

Operações naturalmente idempotentes

Algumas operações são idempotentes por natureza e não precisam de mecanismos adicionais:

✅ Naturalmente idempotentes:
  PUT /users/123 {name: "María"}     → Sempre deixa o mesmo estado
  DELETE /orders/456                   → Apagar algo já apagado = no-op
  GET /products/789                    → Leitura, sem efeitos colaterais

❌ NÃO idempotentes (precisam de mecanismo):
  POST /payments {amount: 100}         → Cada execução cria um pagamento novo
  POST /orders {items: [...]}          → Cada execução cria um pedido novo
  POST /notifications {msg: "Olá"}     → Cada execução envia uma mensagem

Vantagens

  • Retentativas seguras: Os clientes podem tentar novamente sem medo de efeitos duplicados
  • Processamento confiável de eventos: Os consumidores lidam com duplicados corretamente
  • Experiência do usuário: O clique duplo não causa problemas
  • Consistência de dados: Não há registros duplicados nem cobranças em dobro
  • Simplifica a resiliência: O padrão Retry funciona corretamente quando o downstream é idempotente
  • Auditoria: O registro de idempotency keys serve como log de operações

Trade-offs / Desvantagens

  • Armazenamento adicional: Você precisa salvar as keys e seus resultados
  • Complexidade de implementação: Lidar com estados concorrentes (doing/done) exige cuidado
  • Limpeza de keys: As keys antigas devem ser limpas periodicamente (TTL)
  • Geração de keys: O cliente deve gerar keys únicas e consistentes
  • Latência: A verificação de duplicados adiciona uma consulta extra por operação
  • Consistência transacional: A key e a operação devem ser salvas na mesma transação

Quando usar

  • Operações de pagamento e transações financeiras
  • Qualquer endpoint que receba retentativas (POST, PATCH com efeitos colaterais)
  • Consumidores de eventos em arquiteturas event-driven
  • Operações disparadas por webhooks (que podem chegar duplicados)
  • Qualquer operação onde um duplicado cause dano ao negócio
  • APIs públicas onde você não controla o comportamento do cliente

Quando evitar

  • Operações somente de leitura (GET) que já são idempotentes
  • Operações PUT/DELETE que são naturalmente idempotentes
  • Sistemas internos com comunicação confiável e sem retentativas
  • Protótipos onde a complexidade adicional não se justifica

Tecnologias e implementações comuns

CategoriaOpções
Armazenamento de keysRedis (com TTL), PostgreSQL, DynamoDB
Headers padrãoIdempotency-Key (Stripe), X-Request-Id, If-Match (ETags)
BibliotecasStripe SDK (idempotência integrada), custom middleware
Brokers com dedupKafka (exactly-once semantics), SQS (deduplication ID)
FrameworksExpress middleware, Spring interceptor, ASP.NET filter

Relação com outros padrões

  • Retry: As retentativas só são seguras se o downstream for idempotente
  • Event-Driven Architecture: Os consumidores de eventos devem ser idempotentes (at-least-once delivery)
  • Outbox Pattern: Garante a publicação de eventos, mas o consumidor ainda precisa de idempotência
  • Saga Pattern: Cada passo da saga deve ser idempotente para lidar com retentativas de compensação
  • Circuit Breaker: Quando o circuito se fecha e as operações são repetidas, a idempotência evita duplicados

Próximos passos

A idempotência é um requisito fundamental para sistemas distribuídos confiáveis. Para entender como compartilhar modelos de forma controlada entre serviços, explore o Shared Kernel. Para ver como a idempotência se aplica em transações distribuídas, confira o Saga Pattern.