Design de Idempotencia - Criando APIs e Processamentos Seguros

12 min leitura | 2024.12.28

O que e Idempotencia

Idempotencia (Idempotency) e a propriedade onde a mesma operacao produz o mesmo resultado, nao importa quantas vezes seja executada. E um conceito importante para garantir confiabilidade em sistemas distribuidos e design de APIs.

Definicao matematica: f(f(x)) = f(x) Aplicar a mesma funcao multiplas vezes produz o mesmo resultado que aplica-la uma vez

Por que a Idempotencia e Importante

Na comunicacao de rede, problemas como os seguintes podem ocorrer.

sequenceDiagram
    participant C as Cliente
    participant S as Servidor

    C->>S: Requisicao
    Note over C,S: Timeout<br/>(A resposta nao chegou)
    Note over C: A requisicao foi processada?<br/>Devo tentar novamente?

Se a operacao for idempotente, e possivel fazer retry com seguranca.

Metodos HTTP e Idempotencia

MetodoIdempotenciaDescricao
GETObtencao de recurso, sem efeitos colaterais
HEADIgual ao GET (sem corpo)
PUTSubstituicao completa do recurso
DELETEExclusao do recurso
POSTCriacao de recurso, com efeitos colaterais
PATCHAtualizacao parcial (depende da implementacao)

PUT vs POST

MetodoResultado
PUT /users/123Nao importa quantas vezes execute, o user 123 permanece no mesmo estado
POST /usersCada execucao pode criar um novo usuario

Chave de Idempotencia (Idempotency Key)

E uma tecnica para alcançar idempotencia mesmo em requisicoes POST.

POST /payments
Idempotency-Key: pay_abc123xyz
Content-Type: application/json

{
  "amount": 5000,
  "currency": "JPY"
}

Exemplo de Implementacao

async function processPayment(req, res) {
  const idempotencyKey = req.headers['idempotency-key'];

  if (!idempotencyKey) {
    return res.status(400).json({ error: 'Idempotency-Key required' });
  }

  // Verificar resultado existente
  const existing = await redis.get(`idempotency:${idempotencyKey}`);
  if (existing) {
    return res.status(200).json(JSON.parse(existing));
  }

  // Obter lock (prevenir execucao simultanea)
  const lock = await acquireLock(idempotencyKey);
  if (!lock) {
    return res.status(409).json({ error: 'Request in progress' });
  }

  try {
    // Executar processamento
    const result = await executePayment(req.body);

    // Salvar resultado (valido por 24 horas)
    await redis.setex(
      `idempotency:${idempotencyKey}`,
      86400,
      JSON.stringify(result)
    );

    return res.status(201).json(result);
  } finally {
    await releaseLock(idempotencyKey);
  }
}

Padroes de Implementacao de Idempotencia

1. Verificacao de Duplicidade por Identificador Unico

async function createOrder(orderData, requestId) {
  // Verificar existente
  const existing = await db.orders.findByRequestId(requestId);
  if (existing) {
    return existing; // Retornar o mesmo resultado
  }

  // Criar novo
  const order = await db.orders.create({
    ...orderData,
    requestId // Salvar identificador unico
  });

  return order;
}

2. Verificacao de Estado

async function cancelOrder(orderId) {
  const order = await db.orders.findById(orderId);

  // Se ja estiver cancelado, nao faz nada
  if (order.status === 'cancelled') {
    return order; // Idempotente
  }

  // Verificar se pode ser cancelado
  if (order.status === 'shipped') {
    throw new Error('Cannot cancel shipped order');
  }

  return await db.orders.update(orderId, { status: 'cancelled' });
}

3. Lock Otimista

async function updateInventory(productId, quantity, version) {
  const result = await db.query(
    `UPDATE inventory
     SET quantity = quantity - $1, version = version + 1
     WHERE product_id = $2 AND version = $3`,
    [quantity, productId, version]
  );

  if (result.rowCount === 0) {
    throw new Error('Concurrent modification detected');
  }
}

Tornando Operacoes Nao-Idempotentes em Idempotentes

Operacao de Incremento

// Nao idempotente
UPDATE balance SET amount = amount + 100 WHERE user_id = 123

// Tornar idempotente (definir valor absoluto)
UPDATE balance SET amount = 5100 WHERE user_id = 123

// Ou gerenciar por ID de transacao
INSERT INTO transactions (id, user_id, amount)
VALUES ('tx_abc', 123, 100)
ON CONFLICT (id) DO NOTHING;

UPDATE balance
SET amount = (SELECT SUM(amount) FROM transactions WHERE user_id = 123)
WHERE user_id = 123;

Envio de Email

async function sendWelcomeEmail(userId, requestId) {
  // Verificar se ja foi enviado
  const sent = await db.emailLogs.findOne({
    userId,
    type: 'welcome',
    requestId
  });

  if (sent) {
    return { status: 'already_sent' };
  }

  // Enviar
  await emailService.send(/* ... */);

  // Registrar log
  await db.emailLogs.create({
    userId,
    type: 'welcome',
    requestId,
    sentAt: new Date()
  });

  return { status: 'sent' };
}

Estrategias de Retry

Backoff Exponencial

async function retryWithBackoff(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;

      // Backoff exponencial + jitter
      const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
      await sleep(delay);
    }
  }
}

Erros que Devem ou Nao Ser Retentados

RetryTipo
✓ PossivelErros 5xx (erro de servidor), timeout, erros temporarios de rede
✗ Nao possivelErros 4xx (erro do cliente), erros de logica de negocio, erros de autenticacao

Resumo

A idempotencia e um principio de design importante para garantir a confiabilidade de sistemas distribuidos e APIs. Utilize padroes como chaves de idempotencia, verificacao de estado e lock otimista para projetar operacoes que possam ser retentadas com seguranca.

← Voltar para a lista