Idempotência: processar duas vezes sem estragar nada

Idempotência: processar duas vezes sem estragar nada

Robson Rabelo - 13 de julho de 2026 - 0 visualizações

A mesma mensagem vai chegar duas vezes: é matemática, não defeito. Idempotência natural, chaves com UNIQUE arbitrando a corrida e o teste que vale ouro: entregue 2x, afirme efeito 1x.

Eis a verdade desconfortável dos sistemas distribuídos: a mesma mensagem vai chegar duas vezes. Não é defeito do seu broker: é matemática. Entre entregar "no máximo uma vez" (e perder mensagens) e "pelo menos uma vez" (e ocasionalmente duplicar), toda mensageria séria escolhe a segunda. O timeout que expirou depois do processamento, o redrive da DLQ, o retry do cliente impaciente: as duplicatas têm mil pais.

A pergunta de engenharia não é "como evitar duplicatas?" (não dá); é "como processar a mesma mensagem N vezes com o efeito de uma?". Isso é idempotência, e é a propriedade que separa sistemas distribuídos confiáveis de geradores de estorno.

O estrago em uma linha

// Consumidor ingênuo: cada entrega duplicada cobra o cliente DE NOVO
async function processar(msg) {
  await gateway.cobrar(msg.clienteId, msg.valor); // 💥
}

Caminho 1: idempotência natural (quando o domínio deixa)

Algumas operações já são idempotentes por natureza: setar um estado absoluto repete sem estrago.

// Não idempotente: cada execução soma de novo
await db.query("UPDATE conta SET saldo = saldo - $1", [valor]);

// Idempotente: repetir produz o mesmo estado final
await db.query("UPDATE pedido SET status = 'PAGO' WHERE id = $1", [id]);

A lição de desenho: sempre que puder, modele comandos como estados-alvo ("marque como pago") em vez de deltas ("subtraia X"). Boa parte da idempotência se ganha de graça na modelagem, como quase tudo em dados.

Caminho 2: a chave de idempotência (o padrão universal)

Quando a operação tem efeito cumulativo (cobrar, enviar, criar), o padrão é registrar um identificador único por operação e recusar repetições. O banco garante a atomicidade com uma constraint UNIQUE:

async function processar(msg) {
  try {
    // A "trava": só passa quem chega primeiro
    await db.query(
      "INSERT INTO operacoes_processadas (chave) VALUES ($1)",
      [msg.idempotencyKey] // ex.: "pagamento:pedido-4193"
    );
  } catch (e) {
    if (e.code === "23505") return; // duplicata: já processado, sai em paz
    throw e;
  }
  await gateway.cobrar(msg.clienteId, msg.valor); // roda no máximo 1 vez
}

Três detalhes que separam o padrão correto do quase-correto:

  1. A chave nasce na origem, junto com a intenção ("pagamento do pedido 4193"), não no consumidor. Retry do produtor reusa a mesma chave.
  2. Verificar-depois-inserir tem corrida; INSERT com UNIQUE (ou INSERT ... ON CONFLICT DO NOTHING) não. Deixe o banco arbitrar.
  3. Registro e efeito na mesma transação quando o efeito é no mesmo banco; quando é externo (gateway!), grave o registro antes e trate o caso "registrei mas falhei no meio" com reconciliação: melhor recusar uma cobrança legítima em retry do que cobrar duas vezes.

É exatamente o mecanismo que as APIs de pagamento expõem aos clientes: o header Idempotency-Key da Stripe é o exemplo canônico documentado (stripe.com/docs/api/idempotent_requests), e sua API pública de escrita merece o mesmo.

Os dois caminhos da idempotência

As perguntas frequentes de quem implementa

  • Por quanto tempo guardar as chaves? Pela janela realista de duplicatas: retenção da fila + margem. 7 a 30 dias cobre a maioria; expire com TTL (o Redis serve bem aqui para janelas curtas, com o banco como registro durável para as críticas).
  • E operações em lote? Uma chave por item, não por lote: o lote reprocessado pula os itens feitos e completa os pendentes.
  • E a leitura? GET já é idempotente; a disciplina vale para escrita (POST especialmente). PUT bem desenhado (estado absoluto) volta ao caminho 1.

O teste que vale ouro

No seu conjunto de testes de integração, inclua o cenário: entregue a mesma mensagem duas vezes e afirme que o efeito aconteceu uma. É um teste barato que documenta a garantia e quebra na cara de quem remover a trava sem entender. Sistemas que passam nesse teste transformam duplicatas (inevitáveis) em não-eventos, e destravam o redrive sem medo.

Idempotência de ponta a ponta (das chaves na origem à reconciliação com terceiros) é parte do desenho assíncrono que fazemos no serviço de Arquitetura de Software.

Qual operação do seu sistema estragaria se rodasse duas vezes? Conte para a IA da Rabelo Digital, aqui no canto da tela, e ela desenha a trava certa para o caso. Sem formulário, sem espera.


Leia também

Compartilhar:

Deixe um Comentário

Categorias

Posts Recentes