Idempotência em sistemas distribuídos: garantindo que a operação não se repita
1. Fundamentos da Idempotência em Sistemas Distribuídos
Em sistemas distribuídos, a idempotência é a propriedade que garante que uma operação possa ser executada múltiplas vezes sem produzir efeitos colaterais diferentes da execução única. Formalmente, uma operação é idempotente se, para qualquer número n de execuções, o estado final do sistema é idêntico ao estado após a primeira execução.
A importância da idempotência cresce exponencialmente em ambientes onde falhas de rede, timeouts e retransmissões são comuns. Quando um cliente envia uma requisição e não recebe resposta devido a um timeout, ele naturalmente tentará novamente. Sem idempotência, essa retransmissão pode causar duplicação de dados, cobranças em dobro ou inconsistências graves.
A diferença fundamental entre métodos HTTP ilustra bem o conceito:
GET /api/users/123 → Idempotente: ler sempre retorna o mesmo recurso
PUT /api/users/123 → Idempotente: atualizar com os mesmos dados não altera o estado
DELETE /api/users/123 → Idempotente: deletar um recurso já deletado é seguro
POST /api/users → NÃO idempotente: cada chamada cria um novo recurso
PATCH /api/users/123 → NÃO idempotente: depende do delta aplicado
O problema central surge quando sistemas de mensageria ou protocolos de transporte implementam retries automáticos. Uma mensagem pode ser entregue múltiplas vezes, e o receptor precisa estar preparado para lidar com duplicatas de forma segura.
2. Estratégias de Implementação de Idempotência no Backend
A estratégia mais comum e robusta para implementar idempotência é o uso de chaves de idempotência (idempotency keys). O cliente gera um token único (UUID v4, por exemplo) para cada requisição e o envia no cabeçalho. O servidor armazena essa chave junto com o resultado da operação.
Modelo de dados para tabela de idempotência:
CREATE TABLE idempotency_keys (
id BIGSERIAL PRIMARY KEY,
key VARCHAR(64) NOT NULL UNIQUE,
response_status INTEGER NOT NULL,
response_body TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL
);
CREATE UNIQUE INDEX idx_idempotency_key ON idempotency_keys(key);
O fluxo de processamento é:
- Cliente gera UUID e envia no cabeçalho
Idempotency-Key: uuid-123 - Servidor verifica se a chave já existe no banco
- Se existe: retorna a resposta armazenada original
- Se não existe: processa a operação, armazena chave + resultado, retorna resposta
- A chave expira após um período configurável (geralmente 24 horas)
Implementação simplificada em pseudocódigo:
function processarRequisicao(request):
chave = request.header("Idempotency-Key")
if chave is null:
return erro("Chave de idempotência obrigatória")
registro = banco.buscarChave(chave)
if registro exists:
return registro.resposta
transacao = banco.iniciarTransacao()
try:
resultado = executarOperacao(request)
banco.salvarChave(chave, resultado.status, resultado.body)
transacao.commit()
return resultado
except:
transacao.rollback()
throw
3. Idempotência em APIs REST: Padrões e Boas Práticas
Embora os métodos HTTP tenham semânticas de idempotência definidas, na prática é necessário implementar proteções adicionais, especialmente para POST em operações críticas.
Exemplo de endpoint com chave de idempotência:
POST /api/pagamentos
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"valor": 150.00,
"moeda": "BRL",
"destinatario": "joao@email.com"
}
O tratamento de respostas deve seguir estas regras:
Cenário 1: Primeira requisição
→ Servidor processa e retorna 201 Created
→ Armazena {chave: "550e...", status: 201, body: {"id": "pag_123"}}
Cenário 2: Requisição duplicada (mesma chave)
→ Servidor encontra chave existente
→ Retorna 200 OK com mesmo body: {"id": "pag_123"}
→ Indica no cabeçalho X-Idempotent-Replayed: true
Cenário 3: Requisição em andamento
→ Servidor bloqueia concorrência com lock na chave
→ Segunda requisição aguarda ou retorna 409 Conflict
4. Garantindo Idempotência em Operações Financeiras e Críticas
Operações financeiras exigem o mais alto nível de garantia de idempotência. Um pagamento duplicado pode causar prejuízos significativos e danos à reputação.
Exemplo prático: processamento de pagamento com idempotência
function processarPagamento(pagamentoId, chaveIdempotencia, transacao):
// Lock pessimista na chave
banco.executar("SELECT pg_advisory_xact_lock(hashtext(?))", chaveIdempotencia)
// Verifica se já foi processado
pagamento = banco.buscar("SELECT * FROM pagamentos WHERE chave_idempotencia = ?", chaveIdempotencia)
if pagamento exists:
return pagamento
// Verifica saldo e processa
saldo = gatewayFinanceiro.verificarSaldo(pagamentoId)
if saldo < transacao.valor:
throw new SaldoInsuficienteException()
// Operação atômica: debita e registra
banco.executar("""
INSERT INTO pagamentos (id, chave_idempotencia, valor, status)
VALUES (?, ?, ?, 'processado')
""", pagamentoId, chaveIdempotencia, transacao.valor)
gatewayFinanceiro.debitar(pagamentoId, transacao.valor)
return {"status": "sucesso", "id": pagamentoId}
Estratégias de compensação quando a idempotência falha:
Cenário: Timeout durante processamento, cliente reenviou
→ Sistema detecta duplicata pela chave
→ Se pagamento foi debitado mas resposta não retornou:
→ Retorna resposta original armazenada
→ Cliente não tenta novamente
Cenário: Falha catastrófica (banco indisponível)
→ Chave de idempotência perdida
→ Cliente deve reenviar com nova chave
→ Sistema precisa detectar duplicidade por outros meios:
→ Combinação de dados únicos (número do pedido + valor + data)
→ Consulta ao gateway externo
5. Idempotência em Filas e Mensageria Distribuída
Em sistemas de mensageria, a garantia de entrega "exactly-once" é extremamente difícil de alcançar. A maioria dos sistemas oferece "at-least-once", exigindo que os consumidores implementem idempotência.
Exemplo com Apache Kafka:
// Consumidor Kafka com idempotência
function consumirMensagem(mensagem):
idUnico = mensagem.headers["message-id"]
// Verifica se mensagem já foi processada
processado = redis.get("processed:" + idUnico)
if processado is not null:
log("Mensagem duplicada ignorada: " + idUnico)
return // Commit sem processar
// Processa a mensagem
resultado = processarNegocio(mensagem.body)
// Marca como processada (com TTL)
redis.setex("processed:" + idUnico, 86400, "1")
// Commit manual
consumer.commitSync()
Para RabbitMQ e SQS:
// Estratégia com banco de dados para deduplicação
function handlerSQS(event):
for record in event.records:
messageId = record.messageId
body = json.parse(record.body)
// Tenta inserir com UNIQUE constraint
try:
banco.executar("""
INSERT INTO mensagens_processadas (message_id, dados, processado_em)
VALUES (?, ?, NOW())
""", messageId, body)
processarMensagem(body)
except UniqueViolation:
log("Mensagem já processada: " + messageId)
continue
6. Desafios e Armadilhas Comuns na Prática
Problemas de concorrência: Duas requisições simultâneas com a mesma chave de idempotência podem ambas passar pela verificação inicial antes de qualquer uma persistir o resultado. Soluções incluem:
// Uso de locks otimistas com versão
UPDATE idempotency_keys
SET response_status = ?, response_body = ?
WHERE key = ? AND version = 1;
// Se 0 linhas afetadas, outra requisição já processou
Sincronização de relógios: Se chaves de idempotência expiram baseadas em timestamp, diferenças de relógio entre serviços podem causar expiração prematura. Solução: usar TTL baseado no tempo de criação do registro, não no relógio do cliente.
Casos extremos:
Falha no armazenamento da chave:
→ Banco cai após processar operação mas antes de salvar chave
→ Cliente reenvia → operação é processada novamente
→ Mitigação: usar transações distribuídas ou padrão Saga
Rede particionada:
→ Cliente recebe timeout, reenvia com nova chave
→ Primeira requisição chega atrasada e é processada
→ Duas operações executadas
→ Mitigação: timeout longo + chave única por recurso
7. Testes e Validação de Idempotência
Testes de integração para verificar idempotência:
// Teste: requisição duplicada deve retornar mesmo resultado
funcao testarIdempotencia():
chave = gerarUUID()
// Primeira chamada
resposta1 = api.post("/pagamentos", {
headers: {"Idempotency-Key": chave},
body: {"valor": 100}
})
assert resposta1.status == 201
// Segunda chamada com mesma chave
resposta2 = api.post("/pagamentos", {
headers: {"Idempotency-Key": chave},
body: {"valor": 100}
})
assert resposta2.status == 200
assert resposta2.body.id == resposta1.body.id
assert resposta2.headers["X-Idempotent-Replayed"] == "true"
Simulação de falhas de rede:
// Teste: timeout na primeira tentativa
funcao testarTimeoutEIdempotencia():
chave = gerarUUID()
// Simula falha de rede
mockServidor.ativarTimeout()
try:
api.post("/operacao", {headers: {"Idempotency-Key": chave}})
catch TimeoutException:
log("Timeout simulado")
mockServidor.desativarTimeout()
// Retry com mesma chave
resposta = api.post("/operacao", {headers: {"Idempotency-Key": chave}})
assert resposta.status in [200, 201]
assert banco.contarOperacoes(chave) == 1
Monitoramento e métricas:
// Métricas essenciais para dashboard
idempotency_requests_total{status="first"} // Primeiras requisições
idempotency_requests_total{status="replayed"} // Requisições duplicadas
idempotency_key_conflicts_total // Conflitos de concorrência
idempotency_key_expired_total // Chaves expiradas antes do uso
// Logging estruturado
log.info("Requisição idempotente processada", {
"idempotency_key": chave,
"replayed": true,
"original_timestamp": registro.created_at,
"processing_time_ms": elapsed
})
Referências
-
Idempotency - AWS Documentation — Guia prático da AWS sobre como construir APIs idempotentes seguras para retries, com exemplos de implementação.
-
Idempotent Consumer Pattern - Microsoft Azure — Padrão de arquitetura para consumidores idempotentes em sistemas de mensageria, com diagramas e código de exemplo.
-
Stripe API Idempotency — Documentação oficial da Stripe sobre como implementam idempotência em sua API de pagamentos, referência de mercado.
-
Idempotency in Distributed Systems - Martin Fowler — Artigo do Martin Fowler sobre o padrão Idempotent Receiver, com discussão profunda sobre desafios de concorrência.
-
Exactly-Once Processing in Kafka — Blog da Confluent explicando como Kafka implementa semânticas exactly-once e o papel da idempotência.
-
Idempotency-Key HTTP Header Standard — Draft do IETF para padronização do cabeçalho Idempotency-Key em APIs HTTP.
-
PostgreSQL Advisory Locks for Idempotency — Documentação oficial do PostgreSQL sobre locks consultivos, técnica usada para evitar corridas críticas em implementações de idempotência.