Idempotência: operações seguras de repetir

1. Fundamentos da Idempotência

1.1 Definição formal

Idempotência é a propriedade de uma operação produzir o mesmo resultado independentemente do número de vezes que é executada. Formalmente, uma função f é idempotente se f(f(x)) = f(x). Em sistemas de software, isso significa que executar uma requisição uma vez ou múltiplas vezes produz o mesmo efeito colateral observável.

1.2 Diferença entre idempotência e segurança

Em APIs REST, "segurança" (safe methods) refere-se a operações que não alteram estado (GET, HEAD, OPTIONS). Idempotência é diferente: uma operação pode alterar estado, mas deve fazê-lo de forma que repetições não causem efeitos adicionais. DELETE é idempotente (após a primeira exclusão, repetições retornam o mesmo resultado), mas não é seguro (altera estado).

1.3 Exemplos clássicos

GET /users/123        → Idempotente e seguro
PUT /users/123        → Idempotente (substitui recurso)
DELETE /users/123     → Idempotente (remove recurso)
POST /users           → Não idempotente (cria novo recurso)

2. Por que Idempotência é Crucial em Arquiteturas Distribuídas

2.1 Problemas de redes não confiáveis

Em redes distribuídas, timeouts e retransmissões são inevitáveis. Um cliente pode enviar uma requisição, o servidor processá-la com sucesso, mas a resposta se perder. O cliente reenvia a requisição. Sem idempotência, isso pode causar duplicação de pedidos, cobranças ou inconsistências.

2.2 Garantia de consistência em escalabilidade horizontal

Com múltiplas instâncias de serviço, uma requisição pode ser roteada para diferentes servidores em tentativas sucessivas. Idempotência garante que, independentemente de qual instância processa a requisição, o estado final seja consistente.

2.3 Impacto em filas de mensagens

Sistemas de mensageria frequentemente usam entrega "at-least-once" (pelo menos uma vez). Sem idempotência, isso se traduz em processamento duplicado. Idempotência permite tratar "at-least-once" como "exactly-once" na prática.

# Exemplo: processamento de pagamento com idempotência
{
  "idempotency_key": "pagamento-2024-01-abc123",
  "amount": 100.00,
  "account": "user-456"
}

3. Padrões de Implementação de Idempotência

3.1 Chave de idempotência (Idempotency Key)

O cliente gera um identificador único (UUID) e o envia com a requisição. O servidor armazena o resultado associado à chave. Requisições duplicadas com a mesma chave retornam o resultado original.

# Armazenamento em Redis com TTL
SET idempotency:pagamento-2024-01-abc123 
    '{"status": "processed", "transaction_id": "txn-789"}'
    EX 86400  # Expira em 24 horas

3.2 Idempotência baseada em estado

Antes de executar uma operação, verifica-se se o resultado já existe. Se sim, retorna-o sem executar novamente.

# Verificação de estado antes da execução
function createUser(email, name) {
    user = db.users.findByEmail(email)
    if (user) return user  // Já existe, retorna existente
    return db.users.insert({email, name})
}

3.3 Idempotência baseada em versão (Optimistic Locking)

Usa um campo de versão ou timestamp para garantir que apenas uma atualização seja aplicada.

UPDATE accounts 
SET balance = balance - 100, version = version + 1 
WHERE id = 123 AND version = 5
-- Se version != 5, a atualização não ocorre (conflito)

4. Idempotência em APIs REST

4.1 Métodos HTTP idempotentes

GET    /users/123     → Sempre retorna o mesmo recurso
PUT    /users/123     → Substitui recurso, múltiplas execuções = mesmo estado
DELETE /users/123     → Após primeira exclusão, retorna 404 ou 200
PATCH  /users/123     → Idempotente se usar operações absolutas (ex: set)

4.2 Implementando POST idempotente com cabeçalho Idempotency-Key

POST /api/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{
  "amount": 250.00,
  "currency": "BRL"
}

# Resposta original (primeira execução):
HTTP 201 Created
{
  "payment_id": "pay_abc123",
  "status": "confirmed"
}

# Resposta para requisição duplicada:
HTTP 200 OK  # Ou 409 Conflict, dependendo da implementação
{
  "payment_id": "pay_abc123",
  "status": "confirmed"
}

4.3 Tratamento de respostas duplicadas

O servidor deve retornar o mesmo resultado da primeira execução, não executar novamente a operação. Isso exige armazenamento temporário dos resultados.

5. Idempotência em Operações de Banco de Dados

5.1 UPSERT como operação idempotente

INSERT INTO orders (order_id, status, amount)
VALUES ('ord-123', 'confirmed', 500.00)
ON CONFLICT (order_id) 
DO UPDATE SET status = EXCLUDED.status;
-- Se já existe, atualiza para o mesmo valor (idempotente)

5.2 Transações atômicas e compare-and-swap

BEGIN;
SELECT balance FROM accounts WHERE id = 123 FOR UPDATE;
-- Verifica saldo suficiente
UPDATE accounts 
SET balance = balance - 100 
WHERE id = 123 AND balance >= 100;
COMMIT;
-- Se balance < 100, nenhuma linha é afetada (idempotente)

5.3 Idempotência em sharding

Em bancos sharded, a chave de idempotência deve incluir o shard key para garantir que requisições duplicadas sejam roteadas para o mesmo shard.

6. Idempotência em Sistemas de Mensageria e Eventos

6.1 Deduplicação de mensagens

# Tabela de deduplicação
CREATE TABLE processed_messages (
    message_id VARCHAR(64) PRIMARY KEY,
    processed_at TIMESTAMP DEFAULT NOW()
);

# Processamento com verificação
INSERT INTO processed_messages (message_id) VALUES ('msg-456')
ON CONFLICT (message_id) DO NOTHING;
-- Se já existe, a mensagem já foi processada

6.2 Processamento idempotente em Kafka

No Kafka, cada mensagem tem um offset único. O consumidor pode armazenar offsets processados e usar a chave da mensagem para deduplicação.

6.3 Sagas e compensações idempotentes

Operações de rollback em sagas devem ser idempotentes para lidar com falhas durante a compensação.

# Compensação idempotente
function cancelOrder(orderId) {
    order = db.orders.findById(orderId)
    if (order.status == 'cancelled') return  // Já cancelado
    db.orders.update(orderId, {status: 'cancelled'})
    db.payments.refund(orderId)  // Deve ser idempotente
}

7. Monitoramento e Validação de Idempotência

7.1 Métricas importantes

  • Taxa de duplicatas detectadas: duplicates_detected / total_requests
  • Tempo de vida de chaves de idempotência: média e percentis
  • Latência de verificação de idempotência

7.2 Logs e traces

# Log de requisição duplicada
2024-01-15 10:30:45 [WARN] Duplicate request detected
  idempotency_key: "550e8400-e29b-41d4-a716-446655440000"
  original_response: {"payment_id": "pay_abc123", "status": "confirmed"}
  returned cached result

7.3 Testes de idempotência

Estratégias de teste incluem replay de requisições, simulação de timeouts e testes de concorrência.

8. Armadilhas e Boas Práticas

8.1 Efeitos colaterais não idempotentes

Cuidado com logs incrementais, notificações por email e cobranças. Se uma operação for duplicada, esses efeitos colaterais também devem ser idempotentes.

8.2 Limpeza de chaves de idempotência

Use TTL adequado (geralmente 24-48 horas) e implemente garbage collection para chaves expiradas.

8.3 Decisão arquitetural

  • Idempotência no cliente: cliente gera e gerencia chaves
  • Idempotência no servidor: servidor extrai chave da requisição
  • Idempotência em middleware: gateway ou proxy gerencia idempotência

A escolha depende do controle que se tem sobre os clientes e da criticidade da consistência.

Referências