Como implementar idempotency keys em operações financeiras
1. Fundamentos da Idempotência em Sistemas Financeiros
Idempotência é a propriedade de uma operação que, quando executada múltiplas vezes, produz o mesmo resultado que uma única execução. Em sistemas financeiros, essa característica não é opcional — é uma exigência fundamental para garantir integridade transacional.
Operações idempotentes incluem consultas de saldo (GET) ou cancelamentos já processados. Já operações não-idempotentes, como débitos em conta ou transferências, podem causar danos catastróficos se repetidas acidentalmente: cobranças duplicadas, inconsistência de saldos e exposição a fraudes. Um estudo da Stripe estima que 0,1% das transações financeiras globais sofrem duplicação não intencional, resultando em bilhões em prejuízos anuais.
2. O Conceito de Idempotency Key (Chave de Idempotência)
Uma idempotency key é um identificador único que acompanha cada requisição ao servidor. O cliente gera essa chave antes de enviar a operação, e o servidor a armazena junto com o resultado da primeira execução. Em requisições subsequentes com a mesma chave, o servidor retorna o resultado original, sem reprocessar a operação.
O ciclo de vida típico:
1. Cliente gera chave (ex.: UUID v4)
2. Envia no cabeçalho HTTP Idempotency-Key
3. Servidor verifica existência no cache/banco
4. Se nova: processa e armazena resultado com a chave
5. Se existente: retorna resultado armazenado
Exemplos de chaves comuns:
- UUID v4: 550e8400-e29b-41d4-a716-446655440000
- Hash do payload: SHA256(payload + timestamp)
- Combinação: user_id + timestamp + nonce
3. Projeto da API com Suporte a Idempotency Keys
A estrutura de cabeçalhos HTTP deve ser padronizada:
POST /api/v1/pagamentos HTTP/1.1
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"valor": 150.00,
"conta_origem": "12345-6",
"conta_destino": "78901-2"
}
Resposta para primeira execução (201 Created):
HTTP/1.1 201 Created
Idempotency-Replayed: false
Resposta para repetição (200 OK):
HTTP/1.1 200 OK
Idempotency-Replayed: true
Endpoints críticos que exigem idempotência: POST /pagamentos, POST /transferencias, POST /estornos, POST /agendamentos. GETs são naturalmente idempotentes e não exigem chave.
4. Armazenamento e Expiração das Chaves
Banco relacional (PostgreSQL):
CREATE TABLE idempotency_keys (
id SERIAL PRIMARY KEY,
key_value VARCHAR(64) UNIQUE NOT NULL,
endpoint VARCHAR(255) NOT NULL,
request_body TEXT,
response_body TEXT,
response_status INTEGER,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP
);
CREATE INDEX idx_idempotency_key ON idempotency_keys(key_value);
Redis (recomendado para baixa latência):
SET idempotency:550e8400-e29b-41d4-a716-446655440000 '{"status":201,"body":"..."}' EX 86400 NX
TTL recomendado para operações financeiras: 24 horas (86400 segundos). Período inferior pode causar problemas em retentativas atrasadas; superior aumenta armazenamento desnecessário. Para auditoria, registros expirados podem ser movidos para armazenamento frio.
5. Implementação Passo a Passo no Backend
Fluxo de validação com Redis (Node.js):
const express = require('express');
const redis = require('redis');
const { v4: uuidv4 } = require('uuid');
const app = express();
const client = redis.createClient();
async function idempotencyMiddleware(req, res, next) {
const key = req.headers['idempotency-key'];
if (!key) {
return res.status(400).json({ error: 'Idempotency-Key header required' });
}
const redisKey = `idempotency:${key}:${req.path}`;
const existing = await client.get(redisKey);
if (existing) {
const cached = JSON.parse(existing);
return res.status(cached.status).json(cached.body);
}
// Lock para evitar condição de corrida
const lock = await client.setnx(`lock:${key}`, '1');
if (!lock) {
return res.status(409).json({ error: 'Request in progress' });
}
await client.expire(`lock:${key}`, 5); // Timeout de 5 segundos
// Armazena resultado após processamento
const originalJson = res.json.bind(res);
res.json = function(body) {
const data = { status: res.statusCode, body };
client.setex(redisKey, 86400, JSON.stringify(data));
client.del(`lock:${key}`);
originalJson.call(this, body);
};
next();
}
app.post('/api/pagamentos', idempotencyMiddleware, (req, res) => {
// Lógica de pagamento...
res.status(201).json({ id: uuidv4(), status: 'processed' });
});
Implementação com PostgreSQL e transação atômica (Python):
from flask import Flask, request, jsonify
import psycopg2
import uuid
app = Flask(__name__)
def process_with_idempotency(key, endpoint, process_func):
conn = psycopg2.connect(database="financeiro")
try:
with conn:
with conn.cursor() as cur:
# SELECT FOR UPDATE para lock
cur.execute("""
SELECT response_status, response_body
FROM idempotency_keys
WHERE key_value = %s AND endpoint = %s
FOR UPDATE
""", (key, endpoint))
existing = cur.fetchone()
if existing:
return existing[0], existing[1]
# Processa a operação
status, body = process_func()
cur.execute("""
INSERT INTO idempotency_keys
(key_value, endpoint, response_status, response_body, expires_at)
VALUES (%s, %s, %s, %s, NOW() + INTERVAL '24 hours')
""", (key, endpoint, status, jsonify(body).get_data(as_text=True)))
return status, body
finally:
conn.close()
6. Tratamento de Casos Especiais em Operações Financeiras
Isolamento por recurso: Uma mesma chave não deve ser reutilizada em endpoints diferentes. A solução é concatenar o identificador do recurso na chave:
chave_composta = f"{idempotency_key}:{endpoint}:{payment_id}"
Falhas parciais: Se o servidor recebe a requisição, processa parcialmente e cai antes de responder, o cliente retentará com a mesma chave. O servidor deve detectar o estado inconsistente e completar ou reverter a operação.
Operações assíncronas: Para filas e webhooks, a chave deve ser armazenada antes de enfileirar. O processador da fila verifica a chave antes de executar:
// Worker de fila
async function processPayment(job) {
const { idempotencyKey, paymentData } = job.data;
const processed = await redis.get(`processed:${idempotencyKey}`);
if (processed) return; // Já processado
// Processa pagamento...
await redis.set(`processed:${idempotencyKey}`, '1', 'EX', 86400);
}
7. Testes e Validação da Implementação
Cenários essenciais de teste:
1. Requisição única → 201 Created, Idempotency-Replayed: false
2. Requisição duplicada imediata → 200 OK, Idempotency-Replayed: true
3. Requisição duplicada após 1 hora → 200 OK (dentro do TTL)
4. Chave inválida (formato incorreto) → 400 Bad Request
5. Chave expirada → 201 Created (nova operação)
6. Concorrência (duas requisições simultâneas com mesma chave) → uma recebe 201, outra 409
Exemplo com Jest:
describe('Idempotency Key Tests', () => {
test('deve rejeitar requisição sem chave', async () => {
const response = await request(app)
.post('/api/pagamentos')
.send({ valor: 100 });
expect(response.status).toBe(400);
});
test('deve processar apenas uma vez', async () => {
const key = uuidv4();
const first = await request(app)
.post('/api/pagamentos')
.set('Idempotency-Key', key)
.send({ valor: 100 });
const second = await request(app)
.post('/api/pagamentos')
.set('Idempotency-Key', key)
.send({ valor: 100 });
expect(first.status).toBe(201);
expect(second.status).toBe(200);
expect(second.headers['idempotency-replayed']).toBe('true');
});
});
8. Boas Práticas e Considerações Finais
Documentação para consumidores: Forneça exemplos claros de como gerar e enviar chaves. Especifique o formato aceito (UUID v4, string de até 64 caracteres) e o TTL.
Combinação com segurança: Rate limiting impede abuso de chaves falsas. Autenticação forte (OAuth 2.0, mTLS) protege contra injeção de chaves maliciosas.
Limitações importantes: Idempotency keys não resolvem todos os problemas de consistência em sistemas distribuídos. Para cenários que exigem garantias mais fortes, considere padrões como Sagas, Two-Phase Commit ou eventos de compensação.
Implementar idempotency keys corretamente é um investimento que reduz drasticamente incidentes financeiros, aumenta a confiança dos clientes e simplifica a arquitetura de retentativas. Comece pelos endpoints críticos, teste exaustivamente e monitore a taxa de rejeição por chaves duplicadas — ela indica a saúde do sistema.
Referências
- Stripe: Idempotent Requests — Documentação oficial da Stripe sobre implementação de idempotency keys em APIs financeiras
- AWS: Idempotency in API Design — Guia da AWS sobre design de APIs idempotentes para sistemas críticos
- Redis: SETNX Command Documentation — Documentação oficial do comando usado para locks atômicos em implementações com Redis
- PostgreSQL: Row-Level Locking — Guia oficial de locking no PostgreSQL para transações concorrentes
- Microsoft: Idempotency Patterns in Distributed Systems — Padrões de idempotência para sistemas distribuídos da Microsoft Architecture Center