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