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
| Armazenamento | Vantagens | Desvantagens |
|---|---|---|
| Banco de dados | Persistente, transacional | Mais lento, requer limpeza |
| Redis/Cache | Rápido, TTL automático | Pode perder dados se reiniciar |
| Tabela dedicada | Separação clara, fácil de consultar | Tabela 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
| Categoria | Opções |
|---|---|
| Armazenamento de keys | Redis (com TTL), PostgreSQL, DynamoDB |
| Headers padrão | Idempotency-Key (Stripe), X-Request-Id, If-Match (ETags) |
| Bibliotecas | Stripe SDK (idempotência integrada), custom middleware |
| Brokers com dedup | Kafka (exactly-once semantics), SQS (deduplication ID) |
| Frameworks | Express 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.